✨ feat(vscode): 会话 tab 可交接对话给同事

This commit is contained in:
cruldra committed 2026-08-27 15:14:42 +08:00
1 parent 5ee8226167
commit 77037924ef
20 files changed
+938 -26

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
import { describe, expect, it } from 'vitest'
import {
buildSessionHandoffManifest,
isMailboxIssue,
MAILBOX_MARKER,
MAILBOX_TITLE,
parseSessionHandoffAttachmentName,
parseSessionHandoffManifest,
sessionHandoffAttachmentName,
slugSessionName,
} from './sessionHandoff'
const SID = '11111111-1111-4111-8111-111111111111'
describe('isMailboxIssue', () => {
it('title 命中', () => {
expect(isMailboxIssue({ title: MAILBOX_TITLE })).toBe(true)
})
it('label 命中', () => {
expect(isMailboxIssue({ title: '其他', labels: [{ name: 'spx-session-mailbox' }] })).toBe(true)
})
it('body marker 命中', () => {
expect(isMailboxIssue({ title: 'x', body: `hello ${MAILBOX_MARKER}` })).toBe(true)
})
it('普通工单不是', () => {
expect(isMailboxIssue({ title: '修登录', body: 'foo', labels: [{ name: 'bug' }] })).toBe(false)
})
})
describe('sessionHandoffAttachmentName', () => {
it('编解码往返', () => {
const name = sessionHandoffAttachmentName({
sid: SID,
from: 'chw',
to: 'cruldra',
name: '测试服初始化',
})
expect(name).toBe(`spx-handoff-session!${SID}!chw!cruldra!测试服初始化.tgz`)
expect(parseSessionHandoffAttachmentName(name)).toEqual({
sid: SID,
from: 'chw',
to: 'cruldra',
name: '测试服初始化',
})
})
it('空名字也合法', () => {
const name = sessionHandoffAttachmentName({ sid: SID, from: 'a', to: 'b', name: '' })
expect(parseSessionHandoffAttachmentName(name)).toEqual({
sid: SID,
from: 'a',
to: 'b',
name: '',
})
})
it('slug 去掉非法字符', () => {
expect(slugSessionName('foo/bar baz')).toBe('foo_bar_baz')
})
it('sid 非 UUID → 抛错', () => {
expect(() => sessionHandoffAttachmentName({ sid: '../x', from: 'a', to: 'b', name: '' })).toThrow(/UUID/)
})
it('login 含 ! → 抛错', () => {
expect(() => sessionHandoffAttachmentName({ sid: SID, from: 'a!b', to: 'c', name: '' })).toThrow(/from/)
})
it('工单移交包名解析不到', () => {
expect(parseSessionHandoffAttachmentName('spx-handoff-issue-12.tgz')).toBeUndefined()
})
})
describe('parseSessionHandoffManifest', () => {
const good = {
version: 1,
kind: 'managed-session',
from: 'chw',
to: 'cruldra',
createdAt: '2026-08-27T00:00:00Z',
workspacePath: '/home/chw/proj',
session: { id: SID, name: '测试服初始化', profilePath: '/x/offical.json' },
claude: [{ id: SID }],
}
it('合法清单原样返回', () => {
expect(parseSessionHandoffManifest(JSON.stringify(good))).toEqual(good)
})
it('kind 不对 → 抛错', () => {
expect(() => parseSessionHandoffManifest(JSON.stringify({ ...good, kind: 'issue' }))).toThrow(/kind/)
})
it('缺少 workspacePath → 抛错', () => {
const { workspacePath: _, ...rest } = good
expect(() => parseSessionHandoffManifest(JSON.stringify(rest))).toThrow(/workspacePath/)
})
it('claude 空 → 抛错', () => {
expect(() => parseSessionHandoffManifest(JSON.stringify({ ...good, claude: [] }))).toThrow(/claude/)
})
it('build 再 parse 往返', () => {
const built = buildSessionHandoffManifest({
from: 'chw',
to: 'cruldra',
workspacePath: '/home/chw/proj',
session: { id: SID, name: 'n' },
createdAt: '2026-08-27T00:00:00Z',
})
expect(parseSessionHandoffManifest(JSON.stringify(built))).toEqual(built)
})
})
+153
View File
@@ -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 }],
}
}