#!/usr/bin/env node import { execFileSync } from 'node:child_process'; import { mkdir, realpath, rename, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, isAbsolute, resolve } from 'node:path'; import { normalize, readJson, readStdin, tokens } from './intake-lib.mjs'; const baseToken = 'Ed21b9VwNaZfiesxWgDc67wXnoc'; const formShareToken = 'shrcnT6yRL9olcpFkppFkUcKe5e'; const formUrl = 'https://karni9c557h.feishu.cn/share/base/shrcnT6yRL9olcpFkppFkUcKe5e'; const receiptPath = resolve(homedir(), '.easyai-intake', 'receipts.json'); try { const value = normalize(await readStdin()); const identity = tokens(value); const confirmation = argument('--confirm'); const dryRun = process.argv.includes('--dry-run'); if (!confirmation || confirmation !== identity.confirmation_token) throw new Error('确认令牌缺失或已过期,请重新预览并确认'); const receipts = await readJson(receiptPath, {}); if (!dryRun && receipts[identity.submission_key]) { const existing = receipts[identity.submission_key]; if (existing.status_code === 'submitting') throw new Error(`提交 ${identity.submission_key} 的结果待中央调度器核对,请勿重复提交`); console.log(JSON.stringify({ ok: true, duplicate: true, ...existing }, null, 2)); process.exit(0); } const auth = lark(['auth', 'status', '--json', '--verify']); const user = auth.identities?.user; if (!user?.verified || !user.openId) throw new Error('当前电脑未完成飞书用户授权'); const form = lark(['base', '+form-detail', '--share-token', formShareToken, '--as', 'user']); if (form.base_token !== baseToken) throw new Error('飞书表单指向了非预期 Base'); const fields = new Map((form.questions ?? []).map((field) => [field.title, field])); for (const name of ['类型', '标题', '需求背景与目标', '验收标准', '优先级', '产品负责人', '提交幂等键', '提交来源']) if (!fields.has(name)) throw new Error(`受控表单缺少必需字段: ${name}`); const payload = { '类型': value.type, '标题': value.title, '需求背景与目标': value.background, '验收标准': value.acceptance_criteria.map((item, index) => `${index + 1}. ${item}`).join('\n'), '优先级': value.priority, '产品负责人': [{ id: user.openId }], '提交幂等键': identity.submission_key, '提交来源': 'Codex Skill', ...(value.environment ? { '发生环境': value.environment } : {}), ...(value.reproduction_steps ? { '复现步骤': value.reproduction_steps.map((item, index) => `${index + 1}. ${item}`).join('\n') } : {}), ...(value.actual_result ? { '实际结果': value.actual_result } : {}), ...(value.expected_result ? { '期望结果': value.expected_result } : {}), ...(value.impact_scope.length ? { '影响范围': value.impact_scope } : {}), ...(value.desired_at ? { '期望完成时间': value.desired_at } : {}), ...(value.references ? { '参考资料': value.references } : {}), }; for (const attachment of value.attachments) await assertSafePath(attachment); const submission = { fields: payload, ...(value.attachments.length ? { attachments: { '附件': value.attachments } } : {}), }; if (!dryRun) { receipts[identity.submission_key] = { submission_url: formUrl, submission_key: identity.submission_key, status_code: 'submitting', status: '正在提交;若进程中断,请等待中央调度器核对,勿重复提交', }; await writeReceipts(receipts); } const submitted = lark([ 'base', '+form-submit', '--share-token', formShareToken, ...(value.attachments.length ? ['--base-token', baseToken] : []), '--json', JSON.stringify(submission), '--as', 'user', ...(dryRun ? ['--dry-run'] : []), ]); if (dryRun) { console.log(JSON.stringify({ ok: true, dry_run: true, submission_key: identity.submission_key, submitted }, null, 2)); process.exit(0); } const receipt = { submission_url: formUrl, submission_key: identity.submission_key, status_code: 'submitted', status: '已登记,等待系统分配任务编号', }; receipts[identity.submission_key] = receipt; await writeReceipts(receipts); console.log(JSON.stringify({ ok: true, duplicate: false, ...receipt }, null, 2)); } catch (error) { console.error(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : '未知错误' })); process.exitCode = 1; } function lark(args) { const output = execFileSync('lark-cli', args, { encoding: 'utf8', env: { ...process.env, LARKSUITE_CLI_NO_UPDATE_NOTIFIER: '1', LARKSUITE_CLI_NO_SKILLS_NOTIFIER: '1' }, stdio: ['ignore', 'pipe', 'pipe'], }); const envelope = JSON.parse(output); if (envelope.ok === false) throw new Error([envelope.error?.message, envelope.error?.hint].filter(Boolean).join(': ')); return envelope.data ?? envelope; } function argument(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } async function assertSafePath(path) { if (typeof path !== 'string' || !path || isAbsolute(path) || path.split('/').includes('..')) throw new Error(`不安全的附件路径: ${path}`); const root = await realpath(process.cwd()); const target = await realpath(resolve(root, path)); if (target !== root && !target.startsWith(`${root}/`)) throw new Error(`附件超出当前目录: ${path}`); } async function writeReceipts(receipts) { const directory = dirname(receiptPath); const temporary = `${receiptPath}.${process.pid}.tmp`; await mkdir(directory, { recursive: true, mode: 0o700 }); await writeFile(temporary, `${JSON.stringify(receipts, null, 2)}\n`, { mode: 0o600 }); await rename(temporary, receiptPath); }