✨ feat(vscode): 会话 tab 可交接对话给同事
This commit is contained in:
1 parent
5ee8226167
commit
77037924ef
20 files changed
+938
-26
No files matched your search
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 会话交接: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 }],
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user