✨ feat(vscode): 会话交接改走阿里云 OSS 收件箱、换 xz 压缩,另加从 env 文件配置移交存储的 URI
- 会话包传到 OSS 上收件人的收件箱 spx-handoff/<owner>/<repo>/session/<收件人>/<sid>!<发送人>!<会话名>.tar.xz,对象私有;owner/repo/收件人转小写 - 待接收列表就是列自己的收件箱;同一会话再交给同一人先删旧包;接收后删包,包已被别处收走时提示并刷新 - 打包换成 xz 1 级,和工单移交共用 packHandoff / unpackHandoff,删掉 gzip 版 - 删掉邮箱工单整套(mailboxIssue、MAILBOX_*、isMailboxIssue 及看板 / webhook 里的过滤、附件名编解码)和不再用的 Gitea 附件、标签、建工单接口 - session-handoff/accept 与待接收项改用 OSS 对象 key 标识 - 没配移交存储时发送、接收、刷新都提示去配,刷新回空列表 - 进度提示照工单移交:同一条提示依次显示打包、上传、下载多少 MB、解包、装会话 - 新增 URI configure-handoff-store?envFile=<绝对路径>:读 OSS_ENDPOINT / OSS_BUCKET_NAME / OSS_ACCESS_KEY / OSS_SECRET_KEY 保存,和命令共用 saveHandoffStore;URI 入口按 path 统一分发,未知 path 报错
This commit is contained in:
1 parent
81fe14f6dc
commit
fcee4143d4
26 files changed
+542
-515
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 工单移交的会话包放在阿里云 OSS 上:发送方上传,接管方下载后删掉。
|
||||
* 工单移交和会话交接的会话包放在阿里云 OSS 上:发送方上传,接收方下载后删掉。
|
||||
*
|
||||
* 这里只管传文件,不碰 VS Code;配置从哪来见 `src/oss/handoffStoreSetup.ts`。
|
||||
*/
|
||||
@@ -107,6 +107,32 @@ export async function downloadHandoffBundle(
|
||||
throw new Error(`下载不完整:收到 ${received} 字节,应为 ${total} 字节`)
|
||||
}
|
||||
|
||||
/** `err` 是不是「对象不存在」:会话包已经被别人收走或删掉了。 */
|
||||
export function isHandoffBundleMissing(err: unknown): boolean {
|
||||
return typeof err === 'object' && err !== null && (err as { code?: unknown }).code === 'NoSuchKey'
|
||||
}
|
||||
|
||||
export async function deleteHandoffBundle(store: HandoffStore, key: string): Promise<void> {
|
||||
await client(store).delete(key)
|
||||
}
|
||||
|
||||
/** OSS 上的一个会话包。 */
|
||||
export interface HandoffBundleObject {
|
||||
key: string
|
||||
size: number
|
||||
/** ISO 时间,如 `2026-10-08T01:02:03.000Z` */
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
/** 列出 key 以 `prefix` 开头的全部对象。OSS 一页最多给 1000 个,这里翻页直到列完。 */
|
||||
export async function listHandoffBundles(store: HandoffStore, prefix: string): Promise<HandoffBundleObject[]> {
|
||||
const oss = client(store)
|
||||
const out: HandoffBundleObject[] = []
|
||||
let token: string | undefined
|
||||
do {
|
||||
const page = await oss.listV2({ 'prefix': prefix, 'max-keys': 1000, ...(token ? { 'continuation-token': token } : {}) })
|
||||
out.push(...page.objects.map(o => ({ key: o.name, size: o.size, lastModified: o.lastModified })))
|
||||
token = page.isTruncated ? page.nextContinuationToken : undefined
|
||||
} while (token)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseHandoffStoreEnv } from './handoffStoreEnv'
|
||||
|
||||
const STORE = {
|
||||
endpoint: 'oss-cn-beijing.aliyuncs.com',
|
||||
bucket: 'luffy-agent-platform',
|
||||
accessKeyId: 'AK-ID',
|
||||
accessKeySecret: 'AK-SECRET',
|
||||
}
|
||||
|
||||
describe('parseHandoffStoreEnv', () => {
|
||||
it('luffy 后端 .env 的写法:跳过注释、空行和别的变量', () => {
|
||||
const text = [
|
||||
'# 数据库',
|
||||
'DATABASE_URL=postgres://x',
|
||||
'',
|
||||
'OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com',
|
||||
'OSS_ACCESS_KEY=AK-ID',
|
||||
'OSS_SECRET_KEY=AK-SECRET',
|
||||
'OSS_BUCKET_NAME=luffy-agent-platform',
|
||||
'OSS_PUBLIC_BASE=https://cdn.example.com',
|
||||
].join('\n')
|
||||
expect(parseHandoffStoreEnv(text)).toEqual({ ok: true, store: STORE })
|
||||
})
|
||||
|
||||
it('认 CRLF 换行、export 前缀、等号两边空格、成对引号和行尾注释', () => {
|
||||
const text = [
|
||||
'export OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com"',
|
||||
'OSS_BUCKET_NAME=luffy-agent-platform # 正式服',
|
||||
'OSS_ACCESS_KEY=\'AK-ID\'',
|
||||
'OSS_SECRET_KEY=AK-SECRET',
|
||||
].join('\r\n')
|
||||
expect(parseHandoffStoreEnv(text)).toEqual({ ok: true, store: STORE })
|
||||
})
|
||||
|
||||
it('值里的 # 不挨着空格就不算注释', () => {
|
||||
const text = 'OSS_ENDPOINT=e\nOSS_BUCKET_NAME=b\nOSS_ACCESS_KEY=a\nOSS_SECRET_KEY=s#ec+ret/=='
|
||||
const result = parseHandoffStoreEnv(text)
|
||||
expect(result.ok && result.store.accessKeySecret).toBe('s#ec+ret/==')
|
||||
})
|
||||
|
||||
it('endpoint 去掉协议和结尾的 /', () => {
|
||||
const text = 'OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com/\nOSS_BUCKET_NAME=b\nOSS_ACCESS_KEY=a\nOSS_SECRET_KEY=s'
|
||||
const result = parseHandoffStoreEnv(text)
|
||||
expect(result.ok && result.store.endpoint).toBe('oss-cn-beijing.aliyuncs.com')
|
||||
})
|
||||
|
||||
it('同一个变量写两遍以后一遍为准', () => {
|
||||
const text = 'OSS_ENDPOINT=old\nOSS_ENDPOINT=new\nOSS_BUCKET_NAME=b\nOSS_ACCESS_KEY=a\nOSS_SECRET_KEY=s'
|
||||
const result = parseHandoffStoreEnv(text)
|
||||
expect(result.ok && result.store.endpoint).toBe('new')
|
||||
})
|
||||
|
||||
it('缺项和空值都报出变量名', () => {
|
||||
const text = 'OSS_ENDPOINT=e\nOSS_ACCESS_KEY=\n# OSS_SECRET_KEY=s\nOSS_BUCKET_NAME=b'
|
||||
expect(parseHandoffStoreEnv(text)).toEqual({ ok: false, missing: ['OSS_ACCESS_KEY', 'OSS_SECRET_KEY'] })
|
||||
})
|
||||
|
||||
it('空文本四项全缺', () => {
|
||||
expect(parseHandoffStoreEnv('')).toEqual({ ok: false, missing: ['OSS_ENDPOINT', 'OSS_BUCKET_NAME', 'OSS_ACCESS_KEY', 'OSS_SECRET_KEY'] })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 从 env 文件的文本里读移交存储配置。认的是 luffy-agent-platform 后端 `.env` 的写法:
|
||||
* `OSS_ENDPOINT`、`OSS_BUCKET_NAME`、`OSS_ACCESS_KEY`、`OSS_SECRET_KEY` 四行 `KEY=value`,
|
||||
* 空行、`#` 注释和别的变量都跳过。
|
||||
*/
|
||||
|
||||
import type { HandoffStore } from './handoffStore'
|
||||
|
||||
const ENV_NAMES = ['OSS_ENDPOINT', 'OSS_BUCKET_NAME', 'OSS_ACCESS_KEY', 'OSS_SECRET_KEY'] as const
|
||||
|
||||
type EnvName = typeof ENV_NAMES[number]
|
||||
|
||||
const LINE_RE = /^(?:export\s+)?(\w+)\s*=(.*)$/
|
||||
|
||||
/** 读出来的配置,或者缺了哪几个变量(只有变量名,不带值)。 */
|
||||
export type HandoffStoreEnvResult = { ok: true, store: HandoffStore } | { ok: false, missing: EnvName[] }
|
||||
|
||||
/** 去掉值两边成对的引号;没加引号的值去掉 ` #` 起的行尾注释。 */
|
||||
function unquote(raw: string): string {
|
||||
const v = raw.trim()
|
||||
const quote = v[0]
|
||||
if (quote === '"' || quote === '\'') {
|
||||
const end = v.indexOf(quote, 1)
|
||||
if (end > 0)
|
||||
return v.slice(1, end)
|
||||
}
|
||||
return v.replace(/\s#.*$/, '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一个变量写了几遍以最后一遍为准,值为空算缺。endpoint 去掉 `https://` 和结尾的 `/`:
|
||||
* 设置里存的是不带协议的域名,上传下载一律走 https。
|
||||
*/
|
||||
export function parseHandoffStoreEnv(text: string): HandoffStoreEnvResult {
|
||||
const values = new Map<string, string>()
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#'))
|
||||
continue
|
||||
const m = LINE_RE.exec(trimmed)
|
||||
if (m && (ENV_NAMES as readonly string[]).includes(m[1]))
|
||||
values.set(m[1], unquote(m[2]))
|
||||
}
|
||||
const missing = ENV_NAMES.filter(name => !values.get(name))
|
||||
if (missing.length > 0)
|
||||
return { ok: false, missing }
|
||||
const get = (name: EnvName): string => values.get(name) ?? ''
|
||||
return {
|
||||
ok: true,
|
||||
store: {
|
||||
endpoint: get('OSS_ENDPOINT').replace(/^https?:\/\//i, '').replace(/\/+$/, ''),
|
||||
bucket: get('OSS_BUCKET_NAME'),
|
||||
accessKeyId: get('OSS_ACCESS_KEY'),
|
||||
accessKeySecret: get('OSS_SECRET_KEY'),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,15 @@ export async function configureHandoffStore(ctx: ExtensionContext): Promise<void
|
||||
const accessKeySecret = await ask({ title: '配置移交存储(4/4)', prompt: 'AccessKey Secret', value: keys.accessKeySecret, password: true })
|
||||
if (!accessKeySecret)
|
||||
return
|
||||
await saveSettings(ctx, { handoffOssEndpoint: endpoint, handoffOssBucket: bucket })
|
||||
await setHandoffOssKeys(ctx, { accessKeyId, accessKeySecret })
|
||||
void window.showInformationMessage(`移交存储已保存:${bucket}(${endpoint})`)
|
||||
await saveHandoffStore(ctx, { endpoint, bucket, accessKeyId, accessKeySecret })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存移交存储配置并提示保存好了:endpoint、bucket 进扩展设置,两个 AccessKey 进密钥存储。
|
||||
* 「配置移交存储」命令和 `configure-handoff-store` URI 都走这里。
|
||||
*/
|
||||
export async function saveHandoffStore(ctx: ExtensionContext, store: HandoffStore): Promise<void> {
|
||||
await saveSettings(ctx, { handoffOssEndpoint: store.endpoint, handoffOssBucket: store.bucket })
|
||||
await setHandoffOssKeys(ctx, { accessKeyId: store.accessKeyId, accessKeySecret: store.accessKeySecret })
|
||||
void window.showInformationMessage(`移交存储已保存:${store.bucket}(${store.endpoint})`)
|
||||
}
|
||||
Reference in new issue
Block a user