一句话交给模型
prompt 模型拆成几件事
readdraftcheck 到各个系统里动手
ledgercrmbank 结果交回员工定
hand backdecide 采纳退回
底层重塑模型方能入局
把
对
扫清障碍
让系统和生产力重生
让
每
重新
智能体与员工
站在同一层底座上
站在同
模型的每
一张图
钱 事 人 画在同
一双手
让
一道边界
越界即停 权责
一本账
每
边界
搜索单号、客户、供应商
1// 档位是数据里的行 加一档只加一行
2import type { Request, Role, Routed, Signed, Signer, Tier, Trail } from "./types";
3import { CONTRACT_HIGH, CONTRACT_MID, TIER_HIGH, TIER_MID } from "./tiers";
4
5export const TIERS: Readonly<Record<string, readonly Tier[]>> = {
6 purchase: [
7 { id: "R-05", v: 2, above: 0, inclusive: false, chain: ["dept_head"] },
8 { id: "R-06", v: 4, above: TIER_MID, inclusive: false,
9 chain: ["dept_head", "finance"] },
10 { id: "R-07", v: 3, above: TIER_HIGH, inclusive: false,
11 chain: ["dept_head", "finance", "gm"] },
12 ],
13 contract_change: [
14 { id: "R-21", v: 1, above: 0, inclusive: false, chain: ["sales_head"] },
15 { id: "R-22", v: 1, above: CONTRACT_MID, inclusive: false,
16 chain: ["sales_head", "finance"] },
17 { id: "R-23", v: 1, above: CONTRACT_HIGH, inclusive: false,
18 chain: ["sales_head", "finance", "gm"] },
19 ],
20};
21const passes = (t: Tier, amount: number) =>
22 (t.inclusive ? amount >= t.above : amount > t.above); // 分界线是一列数据
23
24export function route(req: Request, tiers: typeof TIERS, trail: Trail,
25 now: () => string): Routed {
26 if (!Number.isInteger(req.amount) || req.amount <= 0)
27 return { ok: false, reason: "invalid_amount", ref: req.id };
28 // 取金额越过的最高一档; 没有匹配的行就不出路由
29 const tier = (tiers[req.kind] ?? []).filter((t) => passes(t, req.amount))
30 .reduce<Tier | undefined>((top, t) => (top && top.above > t.above ? top : t),
31 undefined);
32 if (!tier) return { ok: false, reason: "no_tier", ref: req.id };
33 const rule = `${tier.id} · v${tier.v}`;
34 trail.append({ at: now(), ref: req.id, rule, chain: tier.chain }); // 命中记一笔
35 return { ok: true, chain: tier.chain, rule }; // 每个签字位都留在链上
36}
37
38// 建任务: 人挑出的几张单, 逐张查出该走哪一档; 只查不记(留痕由批的那一步写)
39const QUIET: Trail = { append: () => undefined };
40export const checklist = (reqs: readonly Request[], tiers: typeof TIERS) =>
41 reqs.map((r) => ({ ref: r.id, routed: route(r, tiers, QUIET, () => "") }));
42
43// 签字位由持该角色的人签, 提交人本人不能签。
44export function canSign(req: Request, slot: Role, signer: Signer): Signed {
45 if (signer.id === req.by) return { ok: false, reason: "self_sign" };
46 if (signer.role !== slot) return { ok: false, reason: "wrong_role" };
47 return { ok: true };
48}
49
50// 批完一位轮到链上的下一位; 链走完是 null(流程结束)
51export const nextSlot = (chain: readonly Role[], signed: number): Role | null =>
52 chain[signed] ?? null;
1# 一个入口, 授权数据有几种来源。decide.ts 定死检查顺序:
2# 签字规则 > 拒绝行 > 最高身份 > 允许行 > 汇总的能力集。
3version: 3
4signoff: # 谁能在哪类单据上签哪个位
5 - { doc: purchase, slot: finance, positions: [finance_head] }
6 - { doc: purchase, slot: gm, positions: [gm] }
7 - { doc: quote, slot: review, positions: [sales_head, sales] }
8 - { doc: contract_change, slot: review, positions: [sales_head] } # 合同变更由销售负责人签
9never_signs: [sales] # 不论规则怎么写, 都从每个签字位上剔除
10rows: # 策略行; 主体可以是职位、任命或个人
11 - { id: P-11, effect: deny, who: {position: sales}, action: contract.write }
12 - { id: P-12, effect: deny, who: {person: u-207}, action: payment.write }
13 - { id: P-20, effect: allow, who: {appointment: branch_head}, action: ledger.* }
14 - { id: P-21, effect: allow, who: {position: sales_head}, action: quote.* }
15 - { id: P-30, effect: allow, who: {position: finance_head}, action: approval.return } # 退回发起人
16 - { id: P-31, effect: allow, who: {position: finance_head}, action: approval.exception } # 申请例外, 交总经理裁; 拒绝行照样生效
17top_identities: [owner] # 签字之外全权, 照样留痕
18positions: # 能力集; 个人授权按会话追加
19 sales: [customer.read, customer.write, contract.read]
20 finance_head: [payment.read, payment.write, ledger.read, contract.read]
21 auditor: [payment.read, ledger.read, contract.read, customer.read]
1import { capabilities } from "./policy";
2import type { Actor, Ask, By, Decision, Policy, Trail, Who } from "./policy";
3
4const fits = (pattern: string, action: string) => pattern === action
5 || (pattern.endsWith(".*") && action.startsWith(pattern.slice(0, -1)));
6const holds = (w: Who, a: Actor) =>
7 (w.position ?? w.appointment ?? w.person) !== undefined
8 && (w.position ?? a.position) === a.position && (w.person ?? a.id) === a.id
9 && (!w.appointment || a.appointments.includes(w.appointment));
10
11// 「这个人能不能做这件事」只由这一个入口回答。
12// 每个回答都写明来源(by + ref)和策略版本。
13export function decide(p: Policy, a: Actor, q: Ask): Decision {
14 const d = (allow: boolean, by: By, ref: string): Decision =>
15 ({ allow, by, ref, version: p.version });
16 // 分支可见范围(应用层; 数据库行级权限另算)
17 if (a.branches !== "all" && !a.branches.includes(q.branch))
18 return d(false, "branch_scope", q.branch);
19 if (q.sign) { // 1. 签字规则
20 const { doc, slot } = q.sign;
21 const rule = p.signoff.find((s) => s.doc === doc && s.slot === slot);
22 if (!rule) return d(false, "signoff", `${doc}.${slot}:none`);
23 const ok = rule.positions.includes(a.position)
24 && !p.never_signs.includes(a.position);
25 return d(ok, "signoff", `${doc}.${slot}`);
26 }
27 const mine = p.rows.filter((r) => holds(r.who, a) && fits(r.action, q.action));
28 const deny = mine.find((r) => r.effect === "deny"); // 2. 拒绝行
29 if (deny) return d(false, "deny_row", deny.id);
30 const top = a.identities.find((i) => p.top_identities.includes(i));
31 if (top) return d(true, "top_identity", top); // 3. 最高身份
32 const allow = mine.find((r) => r.effect === "allow"); // 4. 允许行
33 if (allow) return d(true, "allow_row", allow.id);
34 const cap = capabilities(p, a).find((c) => fits(c, q.action));
35 if (cap) return d(true, "capability", cap); // 5. 能力集
36 return d(false, "default", "no_match"); // 都没命中
37}
38
39// 回答写一笔留痕: decide 本身不写; 留痕由调用方注入(和 route 的 trail 一样)
40export function decideAndLog(p: Policy, a: Actor, q: Ask, ref: string,
41 trail: Trail): Decision {
42 const r = decide(p, a, q);
43 trail.append({ ref, by: r.by, rule: r.ref, version: r.version }); // 每次回答记一笔
44 return r;
45}
1// 模型只起草: 读的是扫描过的字段投影, 不写任何控制字段。
2import { z } from "zod";
3import { PII, ruleDraft } from "./harness";
4import type { Draft, Invoice, Model, Reason, Trail } from "./harness";
5
6const INPUT = ["invoice_id", "amount", "currency", "memo"] as const; // 允许清单
7export const project = (row: Invoice): Record<string, string | number> =>
8 Object.fromEntries(INPUT.map((k) => [k, row[k]]));
9const Output = z.object({
10 category: z.enum(["capex", "opex", "transfer"]),
11 summary: z.string().min(1).max(200),
12 cites: z.array(z.string()).min(1),
13}).strict(); // 多出一个键(比如 "amount" 或 "approver")整份作废
14
15export async function draft(row: Invoice, llm: Model, trail: Trail,
16 enabled: boolean): Promise<Draft> {
17 const log = (step: string, detail: string): boolean => {
18 try { trail.append({ ref: row.invoice_id, step, detail }); return true; }
19 catch { return false; }
20 };
21 // 降级: 改用规则文本; 连留痕都失败就明说(不做没有记录的调用)
22 const degrade = (reason: Reason, detail: string = reason): Draft =>
23 ruleDraft(row, log(reason, detail) ? reason : "audit_write_failed");
24 if (!enabled) return degrade("disabled");
25 const input = project(row);
26 const leak = Object.keys(input).find((k) => PII.test(String(input[k])));
27 if (leak) return degrade("pii_in_input", leak); // 进模型之前先扫描
28 let raw: unknown;
29 try { raw = await llm.json(JSON.stringify(input)); }
30 catch (e) { return degrade("model_error", String(e)); }
31 const out = Output.safeParse(raw);
32 if (!out.success)
33 return degrade("schema", out.error.issues.map((i) => i.message).join("; "));
34 if (!log("model_draft", out.data.category))
35 return ruleDraft(row, "audit_write_failed"); // 不留没有记录的模型输出
36 // 控制字段取自结构化的行; 草稿由人批准
37 return { status: "draft", needsApproval: true, mode: "model", reason: null,
38 category: out.data.category, text: out.data.summary,
39 amount: row.amount, approver: row.approver };
40}
code-scenes/scene-1-approval-routing/route.tsLn 1 · UTF-8
