feat: update mode tags and enhance chat session management
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled
Some checks failed
Deploy skills-market-server / deploy (push) Has been cancelled
- Added 'like' mode tag to various components including server, admin interface, and authentication routes for improved user interaction. - Introduced new functions in chat.js for sanitizing agent state and tool calls, enhancing data integrity and management. - Updated .gitignore to include new directories for better file management.
This commit is contained in:
parent
48e999149c
commit
33a59df4ae
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,4 +1,7 @@
|
||||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
scripts/
|
||||
scripts/
|
||||
.omx/
|
||||
updates/
|
||||
!updates/index.html
|
||||
@ -624,14 +624,15 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ALL_MODES = ['chat', 'clarify', 'cowork', 'create', 'video', 'code']
|
||||
const ALL_MODES = ['chat', 'clarify', 'cowork', 'create', 'video', 'code', 'like']
|
||||
const MODE_LABELS = {
|
||||
chat: '对话',
|
||||
clarify: '澄清',
|
||||
cowork: '协作',
|
||||
create: '创作',
|
||||
video: '视频',
|
||||
code: '代码'
|
||||
code: '代码',
|
||||
like: 'Like视频制作'
|
||||
}
|
||||
let token = localStorage.getItem('admin_token') || ''
|
||||
const containerEl = document.querySelector('.container')
|
||||
|
||||
@ -22,7 +22,7 @@ const WHITELIST_EMAILS = (process.env.WHITELIST_EMAILS || '')
|
||||
.map((e) => e.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
const ALL_MODES = ['chat', 'clarify', 'cowork', 'create', 'video', 'code']
|
||||
const ALL_MODES = ['chat', 'clarify', 'cowork', 'create', 'video', 'code', 'like']
|
||||
|
||||
function getDefaultPermissions() {
|
||||
return {
|
||||
|
||||
221
routes/chat.js
221
routes/chat.js
@ -13,6 +13,115 @@ const AGENT_ONLY_BLOCK_TYPES = new Set([
|
||||
'server_tool_use',
|
||||
'web_search_tool_result'
|
||||
])
|
||||
const MAX_AGENT_STATE_BYTES = 512 * 1024
|
||||
const MAX_APPROVED_TOOL_NAMES = 300
|
||||
const MAX_COMPLETED_SUBAGENTS = 120
|
||||
const MAX_SUBAGENT_HISTORY = 240
|
||||
const MAX_SUBAGENT_TOOL_CALLS = 120
|
||||
const MAX_TEXT_CHARS = 12_000
|
||||
|
||||
function clampText(value, max = MAX_TEXT_CHARS) {
|
||||
if (typeof value !== 'string') return ''
|
||||
if (value.length <= max) return value
|
||||
return value.slice(0, max)
|
||||
}
|
||||
|
||||
function asPlainObject(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
||||
return value
|
||||
}
|
||||
|
||||
function sanitizeUsage(value) {
|
||||
const obj = asPlainObject(value)
|
||||
if (!obj) return undefined
|
||||
const out = {}
|
||||
if (typeof obj.inputTokens === 'number') out.inputTokens = obj.inputTokens
|
||||
if (typeof obj.outputTokens === 'number') out.outputTokens = obj.outputTokens
|
||||
if (typeof obj.totalTokens === 'number') out.totalTokens = obj.totalTokens
|
||||
return Object.keys(out).length > 0 ? out : undefined
|
||||
}
|
||||
|
||||
function sanitizeToolCall(value) {
|
||||
const obj = asPlainObject(value)
|
||||
if (!obj || typeof obj.id !== 'string') return null
|
||||
return {
|
||||
id: obj.id,
|
||||
name: typeof obj.name === 'string' ? obj.name : 'UnknownTool',
|
||||
status: typeof obj.status === 'string' ? obj.status : 'completed',
|
||||
input: obj.input,
|
||||
output: obj.output,
|
||||
error: typeof obj.error === 'string' ? clampText(obj.error, 2_000) : undefined,
|
||||
startedAt: typeof obj.startedAt === 'number' ? obj.startedAt : Date.now(),
|
||||
completedAt: typeof obj.completedAt === 'number' ? obj.completedAt : Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeSubAgent(value, sessionId) {
|
||||
const obj = asPlainObject(value)
|
||||
if (!obj || typeof obj.toolUseId !== 'string') return null
|
||||
const toolCalls = Array.isArray(obj.toolCalls)
|
||||
? obj.toolCalls.map(sanitizeToolCall).filter(Boolean).slice(-MAX_SUBAGENT_TOOL_CALLS)
|
||||
: []
|
||||
return {
|
||||
name: typeof obj.name === 'string' ? clampText(obj.name, 200) : 'task',
|
||||
toolUseId: obj.toolUseId,
|
||||
sessionId,
|
||||
isRunning: !!obj.isRunning,
|
||||
iteration: Number.isFinite(obj.iteration) ? Number(obj.iteration) : 0,
|
||||
toolCalls,
|
||||
streamingText: clampText(typeof obj.streamingText === 'string' ? obj.streamingText : ''),
|
||||
resultOutput: clampText(typeof obj.resultOutput === 'string' ? obj.resultOutput : ''),
|
||||
usage: sanitizeUsage(obj.usage),
|
||||
error: typeof obj.error === 'string' ? clampText(obj.error, 2_000) : undefined,
|
||||
startedAt: typeof obj.startedAt === 'number' ? obj.startedAt : Date.now(),
|
||||
completedAt: typeof obj.completedAt === 'number' ? obj.completedAt : null
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAgentState(value, sessionId) {
|
||||
const obj = asPlainObject(value) ?? {}
|
||||
const completedSubAgentsIn = asPlainObject(obj.completedSubAgents) ?? {}
|
||||
const completedEntries = Object.entries(completedSubAgentsIn).slice(-MAX_COMPLETED_SUBAGENTS)
|
||||
const completedSubAgents = {}
|
||||
for (const [key, raw] of completedEntries) {
|
||||
const parsed = sanitizeSubAgent(raw, sessionId)
|
||||
if (parsed) completedSubAgents[key] = parsed
|
||||
}
|
||||
|
||||
const history = Array.isArray(obj.subAgentHistory)
|
||||
? obj.subAgentHistory
|
||||
.map((raw) => sanitizeSubAgent(raw, sessionId))
|
||||
.filter(Boolean)
|
||||
.slice(-MAX_SUBAGENT_HISTORY)
|
||||
: []
|
||||
|
||||
const approvedToolNames = Array.isArray(obj.approvedToolNames)
|
||||
? obj.approvedToolNames
|
||||
.filter((name) => typeof name === 'string')
|
||||
.map((name) => clampText(name, 200))
|
||||
.slice(-MAX_APPROVED_TOOL_NAMES)
|
||||
: []
|
||||
|
||||
const sessionMemoryInjectionBySession = {}
|
||||
const memoryObj = asPlainObject(obj.sessionMemoryInjectionBySession)
|
||||
const row = memoryObj ? asPlainObject(memoryObj[sessionId]) : null
|
||||
if (row) {
|
||||
sessionMemoryInjectionBySession[sessionId] = {
|
||||
snapshotVersion: typeof row.snapshotVersion === 'string' ? row.snapshotVersion : null,
|
||||
snapshotUpdatedAt:
|
||||
typeof row.snapshotUpdatedAt === 'number' ? row.snapshotUpdatedAt : Date.now(),
|
||||
scope: row.scope === 'daily' ? 'daily' : 'user',
|
||||
injectedAt: typeof row.injectedAt === 'number' ? row.injectedAt : Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
completedSubAgents,
|
||||
subAgentHistory: history,
|
||||
approvedToolNames,
|
||||
sessionMemoryInjectionBySession
|
||||
}
|
||||
}
|
||||
|
||||
function stripContentForUserView(contentStr) {
|
||||
if (typeof contentStr !== 'string') return contentStr
|
||||
@ -50,12 +159,14 @@ function mapMessageDoc(doc, view) {
|
||||
function registerChatRoutes(app, db, authRoutes) {
|
||||
const sessionsCol = db.collection('chat_sessions')
|
||||
const messagesCol = db.collection('chat_messages')
|
||||
const agentStateCol = db.collection('chat_session_agent_state')
|
||||
|
||||
const withAuth = (handler) => (req, res) => {
|
||||
authRoutes.verifyToken(req, res, () => handler(req, res))
|
||||
}
|
||||
|
||||
const userObjectId = (req) => new ObjectId(req.user.id)
|
||||
const getOwnedSession = (uid, sessionId) => sessionsCol.findOne({ _id: sessionId, user_id: uid })
|
||||
const assertModeAllowed = (req, res, mode) => {
|
||||
if (!mode) return true
|
||||
if (authRoutes.isModeAllowed(req.user, mode)) return true
|
||||
@ -194,6 +305,7 @@ function registerChatRoutes(app, db, authRoutes) {
|
||||
const uid = userObjectId(req)
|
||||
const id = req.params.id
|
||||
await messagesCol.deleteMany({ session_id: id, user_id: uid })
|
||||
await agentStateCol.deleteOne({ session_id: id, user_id: uid })
|
||||
const r = await sessionsCol.deleteOne({ _id: id, user_id: uid })
|
||||
if (r.deletedCount === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
@ -218,7 +330,7 @@ function registerChatRoutes(app, db, authRoutes) {
|
||||
const limit = Math.min(parseInt(req.query.limit, 10) || 200, 500)
|
||||
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0)
|
||||
|
||||
const sess = await sessionsCol.findOne({ _id: sessionId, user_id: uid })
|
||||
const sess = await getOwnedSession(uid, sessionId)
|
||||
if (!sess) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
}
|
||||
@ -248,7 +360,7 @@ function registerChatRoutes(app, db, authRoutes) {
|
||||
const uid = userObjectId(req)
|
||||
const sessionId = req.params.id
|
||||
const m = req.body || {}
|
||||
const sess = await sessionsCol.findOne({ _id: sessionId, user_id: uid })
|
||||
const sess = await getOwnedSession(uid, sessionId)
|
||||
if (!sess) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
}
|
||||
@ -339,6 +451,108 @@ function registerChatRoutes(app, db, authRoutes) {
|
||||
})
|
||||
)
|
||||
|
||||
// --- Agent State (session scoped) ---
|
||||
app.get(
|
||||
'/api/chat/sessions/:id/agent-state',
|
||||
withAuth(async (req, res) => {
|
||||
try {
|
||||
const uid = userObjectId(req)
|
||||
const sessionId = req.params.id
|
||||
const sess = await getOwnedSession(uid, sessionId)
|
||||
if (!sess) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
}
|
||||
const doc = await agentStateCol.findOne({ session_id: sessionId, user_id: uid })
|
||||
res.json({
|
||||
success: true,
|
||||
state: doc?.state ?? null,
|
||||
revision: typeof doc?.revision === 'number' ? doc.revision : 0,
|
||||
updated_at: typeof doc?.updated_at === 'number' ? doc.updated_at : null
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[Chat] get agent state:', err)
|
||||
res.status(500).json({ success: false, error: err.message })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.put(
|
||||
'/api/chat/sessions/:id/agent-state',
|
||||
withAuth(async (req, res) => {
|
||||
try {
|
||||
const uid = userObjectId(req)
|
||||
const sessionId = req.params.id
|
||||
const sess = await getOwnedSession(uid, sessionId)
|
||||
if (!sess) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
}
|
||||
const body = req.body || {}
|
||||
const baseRevision =
|
||||
typeof body.base_revision === 'number' && Number.isFinite(body.base_revision)
|
||||
? body.base_revision
|
||||
: null
|
||||
const sanitized = sanitizeAgentState(body.state, sessionId)
|
||||
const estimatedSize = Buffer.byteLength(JSON.stringify(sanitized), 'utf8')
|
||||
if (estimatedSize > MAX_AGENT_STATE_BYTES) {
|
||||
return res.status(413).json({
|
||||
success: false,
|
||||
error: `agent state too large (${estimatedSize} bytes)`
|
||||
})
|
||||
}
|
||||
|
||||
const existing = await agentStateCol.findOne({ session_id: sessionId, user_id: uid })
|
||||
const currentRevision = typeof existing?.revision === 'number' ? existing.revision : 0
|
||||
if (baseRevision !== null && baseRevision !== currentRevision) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'Revision conflict',
|
||||
revision: currentRevision,
|
||||
state: existing?.state ?? null
|
||||
})
|
||||
}
|
||||
|
||||
const nextRevision = currentRevision + 1
|
||||
const now = Date.now()
|
||||
await agentStateCol.updateOne(
|
||||
{ session_id: sessionId, user_id: uid },
|
||||
{
|
||||
$set: {
|
||||
session_id: sessionId,
|
||||
user_id: uid,
|
||||
state: sanitized,
|
||||
revision: nextRevision,
|
||||
updated_at: now
|
||||
}
|
||||
},
|
||||
{ upsert: true }
|
||||
)
|
||||
res.json({ success: true, revision: nextRevision, updated_at: now })
|
||||
} catch (err) {
|
||||
console.error('[Chat] put agent state:', err)
|
||||
res.status(500).json({ success: false, error: err.message })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.delete(
|
||||
'/api/chat/sessions/:id/agent-state',
|
||||
withAuth(async (req, res) => {
|
||||
try {
|
||||
const uid = userObjectId(req)
|
||||
const sessionId = req.params.id
|
||||
const sess = await getOwnedSession(uid, sessionId)
|
||||
if (!sess) {
|
||||
return res.status(404).json({ success: false, error: 'Session not found' })
|
||||
}
|
||||
await agentStateCol.deleteOne({ session_id: sessionId, user_id: uid })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
console.error('[Chat] delete agent state:', err)
|
||||
res.status(500).json({ success: false, error: err.message })
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.delete(
|
||||
'/api/chat/sessions/:id/messages/all',
|
||||
withAuth(async (req, res) => {
|
||||
@ -364,9 +578,12 @@ function registerChatRoutes(app, db, authRoutes) {
|
||||
async function ensureChatIndexes(db) {
|
||||
const sessionsCol = db.collection('chat_sessions')
|
||||
const messagesCol = db.collection('chat_messages')
|
||||
const agentStateCol = db.collection('chat_session_agent_state')
|
||||
await sessionsCol.createIndex({ user_id: 1, updated_at: -1 })
|
||||
await messagesCol.createIndex({ session_id: 1, sort_order: 1 })
|
||||
await messagesCol.createIndex({ user_id: 1, session_id: 1 })
|
||||
await agentStateCol.createIndex({ user_id: 1, session_id: 1 }, { unique: true })
|
||||
await agentStateCol.createIndex({ updated_at: -1 })
|
||||
}
|
||||
|
||||
module.exports = { registerChatRoutes, ensureChatIndexes }
|
||||
|
||||
@ -157,7 +157,7 @@ function normalizeTagList(tags) {
|
||||
return out
|
||||
}
|
||||
|
||||
const ROOT_MODE_TAGS = ['clarify', 'cowork', 'create', 'video', 'code']
|
||||
const ROOT_MODE_TAGS = ['clarify', 'cowork', 'create', 'video', 'code', 'like']
|
||||
|
||||
function getAllowedModeTagsFromUser(user) {
|
||||
if (user?.is_root_admin) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user