Files
superwork/vscode/src/cc/sessionHandoff.ts
T

154 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会话交接:mailbox 工单识别、附件文件名、managed-session 清单。
*
* 与工单移交的 handoff.json 分开:那边有 issue/worktree/四种 sid,这边只有一个
* claude 会话。混用解析会把会话包当工单包读崩。
*/
export const MAILBOX_TITLE = 'spx-session-mailbox'
export const MAILBOX_LABEL = 'spx-session-mailbox'
export const MAILBOX_MARKER = '<!-- spx:session-mailbox -->'
export const MAILBOX_BODY = `${MAILBOX_MARKER}
Superwork 会话交接邮箱。不要当任务处理,不要关闭。
`
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const LOGIN_RE = /^[\w.-]+$/
const FILENAME_RE = /^spx-handoff-session!([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})!([\w.-]+)!([\w.-]+)!([^!]*)\.tgz$/i
export interface SessionHandoffManifest {
version: 1
kind: 'managed-session'
from: string
to: string
createdAt: string
workspacePath: string
session: { id: string, name: string, profilePath?: string }
claude: Array<{ id: string }>
}
export interface SessionHandoffAttachmentRef {
sid: string
from: string
to: string
name: string
}
export function isMailboxIssue(issue: {
title?: string
body?: string
labels?: Array<{ name: string }> | null
}): boolean {
if (issue.title === MAILBOX_TITLE)
return true
if ((issue.labels ?? []).some(l => l.name === MAILBOX_LABEL))
return true
if (typeof issue.body === 'string' && issue.body.includes(MAILBOX_MARKER))
return true
return false
}
export function slugSessionName(name: string): string {
const trimmed = name.trim()
if (!trimmed)
return ''
return trimmed.replace(/[^\p{L}\p{N}._-]+/gu, '_').slice(0, 40)
}
export function sessionHandoffAttachmentName(opts: {
sid: string
from: string
to: string
name: string
}): string {
if (!UUID_RE.test(opts.sid))
throw new Error(`会话 id 不是 UUID:${opts.sid}`)
if (!LOGIN_RE.test(opts.from))
throw new Error(`from 含非法字符:${opts.from}`)
if (!LOGIN_RE.test(opts.to))
throw new Error(`to 含非法字符:${opts.to}`)
return `spx-handoff-session!${opts.sid}!${opts.from}!${opts.to}!${slugSessionName(opts.name)}.tgz`
}
export function parseSessionHandoffAttachmentName(filename: string): SessionHandoffAttachmentRef | undefined {
const m = FILENAME_RE.exec(filename)
if (!m)
return undefined
return { sid: m[1], from: m[2], to: m[3], name: m[4] ?? '' }
}
function optString(v: unknown): string | undefined {
return typeof v === 'string' && v.length > 0 ? v : undefined
}
export function parseSessionHandoffManifest(json: string): SessionHandoffManifest {
const raw = JSON.parse(json) as Record<string, unknown>
if (!raw || typeof raw !== 'object')
throw new Error('handoff.json 不是对象')
if (raw.version !== 1)
throw new Error(`handoff.json version 不支持:${String(raw.version)}`)
if (raw.kind !== 'managed-session')
throw new Error(`handoff.json kind 不是 managed-session:${String(raw.kind)}`)
const from = optString(raw.from)
const to = optString(raw.to)
const createdAt = optString(raw.createdAt)
const workspacePath = optString(raw.workspacePath)
if (!from || !to || !createdAt || !workspacePath)
throw new Error('handoff.json 缺少 from / to / createdAt / workspacePath')
if (!LOGIN_RE.test(from) || !LOGIN_RE.test(to))
throw new Error('handoff.json from/to 非法')
const sessionRaw = (raw.session ?? {}) as Record<string, unknown>
const id = optString(sessionRaw.id)
const name = typeof sessionRaw.name === 'string' ? sessionRaw.name : ''
if (!id || !UUID_RE.test(id))
throw new Error(`handoff.json session.id 非法:${String(sessionRaw.id)}`)
const claudeRaw = Array.isArray(raw.claude) ? raw.claude : []
const claude = claudeRaw.map((e) => {
const o = e as Record<string, unknown>
const cid = optString(o.id)
if (!cid || !UUID_RE.test(cid))
throw new Error(`handoff.json claude 条目非法:${JSON.stringify(e)}`)
return { id: cid }
})
if (claude.length === 0)
throw new Error('handoff.json 没有 claude 会话文件')
const profilePath = optString(sessionRaw.profilePath)
return {
version: 1,
kind: 'managed-session',
from,
to,
createdAt,
workspacePath,
session: { id, name, ...(profilePath ? { profilePath } : {}) },
claude,
}
}
export function buildSessionHandoffManifest(opts: {
from: string
to: string
workspacePath: string
session: { id: string, name: string, profilePath?: string }
createdAt?: string
}): SessionHandoffManifest {
if (!UUID_RE.test(opts.session.id))
throw new Error(`会话 id 不是 UUID:${opts.session.id}`)
if (!LOGIN_RE.test(opts.from) || !LOGIN_RE.test(opts.to))
throw new Error('from/to 非法')
return {
version: 1,
kind: 'managed-session',
from: opts.from,
to: opts.to,
createdAt: opts.createdAt ?? new Date().toISOString(),
workspacePath: opts.workspacePath,
session: {
id: opts.session.id,
name: opts.session.name,
...(opts.session.profilePath ? { profilePath: opts.session.profilePath } : {}),
},
claude: [{ id: opts.session.id }],
}
}