@aalis/api-approval 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ace Nyan <ace@acenyan.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # @aalis/api-approval
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 审批契约:模型发起、需要 owner 批准的操作以审批对象的形式发起、推给在场的审批面、由 owner 应答、结算后回给发起方。不含服务实现,默认实现是 `@aalis/plugin-session-server`。
4
+
5
+ ## 角色
6
+
7
+ - `approvals`:服务描述符(服务名 `approvals`)。发起方调 `request(req, signal)` 等结算;审批面经绑定门面 `registerSurface(name)` 登记,随激活撤回,每个客户端 `attend` 一次,在场上报与应答都经它。
8
+ - `Approval`、`ApprovalRequest`、`ApprovalOutcome` 等:审批对象、请求与结局的类型。
9
+ - `approval:created`、`approval:settled`:审批推给审批面、结算时发出的事件。
10
+ - `sanitizeForCard(text)`:审批卡显示事实之前的清洗,各审批面共用;`clipForCard(text, limit)` 清洗并截断,交出开头、结尾与中间省略的字数,「省略 N 字」由审批面按读者语言写。
11
+ - 应答被拒(`ApprovalResponse` 的 `ok: false`)时 `message` 是给应答人看的原因,可以是文字对象(`@aalis/schema-i18n`),审批面按读者语言渲染。
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ pnpm add @aalis/api-approval
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ ```ts
22
+ import { approvals, clipForCard } from '@aalis/api-approval';
23
+ // 审批面自己的字典(@aalis/schema-i18n 的 defineTexts),「省略 N 字」整句在那里
24
+ import { t } from './texts.js';
25
+
26
+ // uses: { approvals: optional(approvals) }
27
+ const surface = caps.approvals.use.registerSurface('cli');
28
+ const attendance = surface.attend({ platform: 'cli', userId: 'console' }, {
29
+ requested: approval => {
30
+ const { head, omitted, tail } = clipForCard(approval.facts.command ?? approval.tool, 400);
31
+ // render:按读者语言把文字对象渲染成字符串
32
+ showCard(omitted === 0 ? head : `${head}${render(t('charsOmitted', { count: omitted }))}${tail}`);
33
+ },
34
+ settled: approvalId => closeCard(approvalId),
35
+ });
36
+ attendance.presence(true);
37
+ ```
38
+
39
+ 详见 [docs/api/api-approval.md](../../docs/api/api-approval.md)。
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @aalis/api-approval — 审批契约
3
+ *
4
+ * 模型发起、需要批准的操作以审批对象的形式出现:发起方发起并等结算,审批对象推给在场的审批面,能批的人应答,结果回给
5
+ * 发起方。审批只有一种,发起方给出这一张谁能批(canAnswer),另标出只有 owner 能批的那种(要 owner 确认的)。本包只有
6
+ * 类型、服务描述符 `approvals` 与卡片清洗函数 `sanitizeForCard`、`clipForCard`;实现见 @aalis/plugin-session-server。
7
+ *
8
+ * 三类使用方:
9
+ * - 发起方(执行守卫):`request(req, signal)` 发起一个审批并等它结算,拒绝时结果里带着回给模型的话;
10
+ * - 审批面(WebUI、终端、IM 私聊与群窗口):经绑定门面 `registerSurface(name)` 登记,随激活撤回。每个客户端 `attend`
11
+ * 一次,在场上报与应答都经它,审计里的审批面取登记身份,不取调用方自报。推给谁、算谁在场、认谁的应答是一条规则:
12
+ * 客户端的身份能批这一张(canAnswer),且它的接入覆盖这张审批({@link AttendScope});
13
+ * - 展示方:`list()` 与 `approval:created`、`approval:settled` 事件。
14
+ */
15
+ import type { TextRef } from '@aalis/schema-i18n';
16
+ /**
17
+ * 发起回合的类别,由发起方(第一方是 plugin-authority 的执行守卫)按这一轮从哪里来算出,不看会话类型与受众,也不判是不是
18
+ * owner(不按授权身份算:owner 建的定时任务会把创建者回填成授权身份):
19
+ * - `hand`:人亲手发来的消息(不带来源,含群里没点名、被频率触发挑中的插话),不论在哪个会话、是不是 owner;还有沿起它的那一轮
20
+ * 回到这样一轮的续做,以及它派出的子任务;
21
+ * - `autonomous`:定时任务、编排、空闲与主动发言、开话题、平台请求事件等其余注入与它们的续做、派出的子任务,以及不在入站回合里的
22
+ * 调用(含编排的工具节点、mcp-server 派出的子任务)。
23
+ *
24
+ * 能不能产生审批由审批服务按类别与参与者判;不能的一律直接拒绝,不推给任何审批面。
25
+ */
26
+ export type OriginKind = 'hand' | 'autonomous';
27
+ /** 发起回合的类别与参与者 */
28
+ export interface ApprovalOrigin {
29
+ kind: OriginKind;
30
+ /** 这一轮的参与者(授权身份) */
31
+ participants: {
32
+ platform: string;
33
+ userId: string;
34
+ }[];
35
+ }
36
+ /**
37
+ * 发起一个审批:要执行的工具与它的参数(目前只有工具这一种目标)。canAnswer 与 ownerOnly 只在进程内交给提供者,不进推给
38
+ * 审批面的审批对象
39
+ */
40
+ export interface ApprovalRequest {
41
+ sessionId: string;
42
+ /** 发起它的回合;回合结束或中止时没结的审批按 cancelled 结算 */
43
+ turnId?: string;
44
+ /** 这次工具调用的 id,客户端据它把审批卡挂到工具卡上 */
45
+ callId?: string;
46
+ origin: ApprovalOrigin;
47
+ tool: string;
48
+ args: Record<string, unknown>;
49
+ /** 命中的规则(如能力声明的确认要求、owner 的确认覆盖),记进审计 */
50
+ rule: string;
51
+ /** 这个人能不能批这一张:参与者审批是他自己就能做这个动作,要 owner 确认的是 owner */
52
+ canAnswer: (who: ApprovalIdentity) => boolean;
53
+ /** 只有 owner 能批的那种(要 owner 确认的,含与参与者审批合成的):只给接入的覆盖用 */
54
+ ownerOnly: boolean;
55
+ }
56
+ /** pending 之后结算为其余四种之一:批准、拒绝或发起不被允许、没人应答(不在场、等满时限)、撤销 */
57
+ export type ApprovalState = 'pending' | 'approved' | 'denied' | 'expired' | 'cancelled';
58
+ /** 结算之后的四种结局 */
59
+ export type SettledState = Exclude<ApprovalState, 'pending'>;
60
+ /**
61
+ * 卡片上的事实,按工具定义上声明的展示种类(api-tools 的 `display`)从参数里取,各项逐字、与执行的参数一致:
62
+ * - `command`、`cwd`:命令类工具声明的命令与工作目录;
63
+ * - `diff`:改文件类工具声明的路径与删、加两侧的参数生成,删的行以 `-` 开头、加的行以 `+` 开头;
64
+ * - `params`:其余参数,逐字。没声明展示种类的工具,全部参数都在这里。
65
+ *
66
+ * 显示之前一律经 {@link sanitizeForCard} 清洗,要截断的经 {@link clipForCard}。
67
+ */
68
+ export interface ApprovalFacts {
69
+ command?: string;
70
+ cwd?: string;
71
+ diff?: {
72
+ path: string;
73
+ patch: string;
74
+ };
75
+ params: Record<string, unknown>;
76
+ }
77
+ export interface Approval {
78
+ /** 由审批服务签发 */
79
+ id: string;
80
+ sessionId: string;
81
+ turnId?: string;
82
+ callId?: string;
83
+ origin: ApprovalOrigin;
84
+ target: 'tool';
85
+ tool: string;
86
+ facts: ApprovalFacts;
87
+ /** 可选的应答;拒绝可以附说明 */
88
+ options: Array<'approve' | 'deny'>;
89
+ /** 请求内容(会话、回合、调用、工具与参数)的哈希,应答时原样带回 */
90
+ contentHash: string;
91
+ /** 到期的时刻(毫秒时间戳),是这张审批自己的事实:审批服务推出时按它的等待时限填,到时没人应答即按过期结算 */
92
+ expiresAt: number;
93
+ state: ApprovalState;
94
+ /**
95
+ * 来源链:所在会话不是根会话时,从根会话往下到它的各级子会话(不含根会话,按层级先后)的 id 与标题(没有标题时是会话名,
96
+ * 子任务没取名的是默认名),审批面据它写是哪个子任务;所在会话就是根会话时没有。审批服务推出时按会话树填
97
+ */
98
+ chain?: {
99
+ id: string;
100
+ title: string | TextRef;
101
+ }[];
102
+ }
103
+ /**
104
+ * 结算从哪里来:
105
+ * - `answer`:能批的人应答(批准或拒绝);
106
+ * - `absent`:没人在场(发起时,或等待中在场的审批面都断开、变为不在场,且宽限内没有恢复);
107
+ * - `timeout`:等满时限;
108
+ * - `ineligible`:发起回合的类别或参与者不能产生审批(起它的那一轮不是人亲手的,或参与者里没有人能批);
109
+ * - `rate-limit`:同一会话树(根会话连同各级子会话)的审批超过频率上限;
110
+ * - `turn-ended`:发起它的回合结束或中止;
111
+ * - `shutdown`:审批服务停止。
112
+ */
113
+ export type ApprovalSource = 'answer' | 'absent' | 'timeout' | 'ineligible' | 'rate-limit' | 'turn-ended' | 'shutdown';
114
+ /** 一个审批怎样结算的:结局、从哪里来、结算的时刻(毫秒时间戳),有人应答的带上应答人 */
115
+ export interface ApprovalSettlement {
116
+ state: SettledState;
117
+ source: ApprovalSource;
118
+ at: number;
119
+ by?: ApprovalIdentity;
120
+ }
121
+ /** 审批的结局。批准时 by 是批的人;拒绝时 message 是回给模型的话:写明被什么拦下、能不能再申请 */
122
+ export type ApprovalOutcome = {
123
+ approvalId: string;
124
+ approved: true;
125
+ by: ApprovalIdentity;
126
+ } | {
127
+ approvalId: string;
128
+ approved: false;
129
+ source: ApprovalSource;
130
+ message: string;
131
+ };
132
+ /** 审批面的客户端收到的推送 */
133
+ export interface ApprovalSink {
134
+ /** 有审批要处理:在场时推出;变为在场时补推还没结的 */
135
+ requested(approval: Approval): void;
136
+ /** 推给过它的审批结算了 */
137
+ settled(approvalId: string, settlement: ApprovalSettlement): void;
138
+ }
139
+ export interface ApprovalAnswer {
140
+ approvalId: string;
141
+ decision: 'approve' | 'deny';
142
+ /** 原样带回审批对象的 contentHash */
143
+ contentHash: string;
144
+ note?: string;
145
+ }
146
+ /**
147
+ * 应答被拒的原因:`forbidden` 应答人不能批这一张;`not-found` 不是本服务签发的 id,或还在等的审批不归这个接入覆盖;
148
+ * `approval-settled` 已经结算(先答先得,后到的都是它,不论接入覆盖不覆盖它);`hash-mismatch` 内容哈希对不上;
149
+ * `unavailable` 审批服务不在
150
+ */
151
+ export type ApprovalRefusal = 'forbidden' | 'not-found' | 'approval-settled' | 'hash-mismatch' | 'unavailable';
152
+ /** 应答的结果。被拒时 message 是给应答人看的原因,可以是文字对象(出口按读者语言渲染) */
153
+ export type ApprovalResponse = {
154
+ ok: true;
155
+ } | {
156
+ ok: false;
157
+ refusal: ApprovalRefusal;
158
+ message: string | TextRef;
159
+ };
160
+ /**
161
+ * 审批面的一个客户端(WebUI 的一条连接、终端、owner 的一个 IM 身份经一个机器人账号、群窗口里的一个人):在场上报与应答都记在
162
+ * 它接入的审批面的登记身份名下
163
+ */
164
+ export interface ApprovalAttendance {
165
+ /**
166
+ * 在场上报。true 表示刚有活动(WebUI 另要页面可见),最近一次这样的上报在时限内即算在场;推给它、还没结的审批也让它一直
167
+ * 算在场。false 表示离开(页面转到后台),即刻不在场:推给它、还没结的审批对它算没收到,它变为在场时补推
168
+ */
169
+ presence(active: boolean): void;
170
+ /**
171
+ * 推给它的这一张没送到它那里(如 IM 里这条消息发不出去):只这一张对它算没收到,它不因这一张算在场,变为在场时补推;推给它的
172
+ * 别的审批照旧。不是推给它、还没结的不起作用
173
+ */
174
+ undelivered(approvalId: string): void;
175
+ /**
176
+ * 它此刻还算不算在场(按上面同一条规则,由审批服务判):审批面据它撤掉不在场的接入,不自己另记时限。撤掉(leave)的接入
177
+ * 收不到推给过它、还没结的卡的结算,还要等结算的先别撤。审批服务不在时不在场
178
+ */
179
+ present(): boolean;
180
+ respond(answer: ApprovalAnswer): ApprovalResponse;
181
+ /** 客户端断开 */
182
+ leave(): void;
183
+ }
184
+ /** 审批面客户端经认证得出的身份:应答人就是它 */
185
+ export interface ApprovalIdentity {
186
+ platform: string;
187
+ userId: string;
188
+ }
189
+ /**
190
+ * 接入的限定,两种:
191
+ * - 群窗口(给 `window`,群窗口的根会话 id;根会话已经定了机器人账号,不再给 selfId):只覆盖所在会话的根会话就是这个窗口的
192
+ * (连同它下面的子会话)、不是只有 owner 能批的审批。群窗口是公开的面,不发只有 owner 能批的;
193
+ * - IM 私聊(不给 `window`):只覆盖只有 owner 能批的、这一轮的参与者里有接入的身份、且所在会话的根会话的机器人账号
194
+ * (`originChat.selfId`)等于 `selfId` 的审批,两边都没有账号算相同
195
+ */
196
+ export interface AttendScope {
197
+ selfId?: string;
198
+ window?: string;
199
+ }
200
+ /** 登记过的审批面 */
201
+ export interface ApprovalSurface {
202
+ /** 登记名(webui、cli、chat) */
203
+ readonly name: string;
204
+ /**
205
+ * 接入一个客户端。给了限定的只覆盖限定的那些审批({@link AttendScope}:IM 私聊每个身份与机器人账号一份,群窗口每个窗口与
206
+ * 发言者一份):只因它们算在场、只收和只能应答它们;不给的覆盖全部,只给 owner 的面(WebUI 的连接、终端):别的身份不限定地
207
+ * 接入会收到各会话里它能批的审批,别人的接入一律限定群窗口。推给它、算它在场、认它的应答另要接入的身份能批这一张
208
+ */
209
+ attend(who: ApprovalIdentity, sink: ApprovalSink, scope?: AttendScope): ApprovalAttendance;
210
+ }
211
+ /** 提供者那一侧的审批面登记;登记撤回时 detach */
212
+ export interface SurfaceBinding {
213
+ attend(who: ApprovalIdentity, sink: ApprovalSink, scope?: AttendScope): ApprovalAttendance;
214
+ detach(): void;
215
+ }
216
+ /** 提供者契约。审批面不直接在这里登记:经 {@link approvals} 的绑定门面登记,随激活撤回 */
217
+ export interface ApprovalService {
218
+ /** 发起一个审批并等它结算。signal 是发起回合的中止信号,中止即按 cancelled 结算 */
219
+ request(req: ApprovalRequest, signal?: AbortSignal): Promise<ApprovalOutcome>;
220
+ /** 已推给审批面、还没结算的审批 */
221
+ list(filter?: {
222
+ sessionId?: string;
223
+ }): Approval[];
224
+ /** 登记审批面;registrant 是登记方的插件实例,审计记在它名下 */
225
+ attachSurface(name: string, registrant: string): SurfaceBinding;
226
+ }
227
+ /** `approvals` 的业务层:调用转发给选中的那一家,审批面登记自动归属这次激活 */
228
+ export interface BoundApprovals extends ApprovalService {
229
+ /**
230
+ * 登记审批面:同一激活同名再登记替换旧的;提供者换人自动重挂,随激活撤回。
231
+ * 提供者不在场时接入的客户端照样拿到句柄:在场上报不生效,应答回 `unavailable`;提供者上线后下一次上报时接上
232
+ */
233
+ registerSurface(name: string): ApprovalSurface;
234
+ }
235
+ export declare const approvals: import("@aalis/core").ServiceDescriptor<ApprovalService, BoundApprovals>;
236
+ declare module '@aalis/core' {
237
+ /** 审批事件(由审批服务的提供者发出) */
238
+ interface AalisEvents {
239
+ /** 一个审批推给了审批面 */
240
+ 'approval:created': [approval: Approval];
241
+ /** 一个推给过审批面的审批结算了,state 是结局 */
242
+ 'approval:settled': [approval: Approval];
243
+ }
244
+ }
245
+ /**
246
+ * 审批卡显示事实之前的清洗:控制符(含 ANSI 转义序列的 ESC)转义成 `\x1B` 的样子显示;双向控制符、零宽字符等
247
+ * 看不见的字符换成 `⟨U+202E⟩` 的样子。换行与制表符保留。原文里的每个字符都显示出来,不隐藏、不改变顺序。
248
+ */
249
+ export declare function sanitizeForCard(text: string): string;
250
+ /** {@link clipForCard} 的结果:清洗过的开头与结尾,中间省略的字数(没有截断时为 0,整段在 head、tail 为空) */
251
+ export interface CardClip {
252
+ head: string;
253
+ omitted: number;
254
+ tail: string;
255
+ }
256
+ /**
257
+ * 清洗(同 {@link sanitizeForCard})并截断:原文超过 limit 个字符时保留开头与结尾共 limit 个字符,交出两段与中间省略的
258
+ * 字数,显示方在两段之间按读者语言写明省略了多少。截断按原文的字符算,不会切开转义或占位符。
259
+ */
260
+ export declare function clipForCard(text: string, limit: number): CardClip;
261
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @aalis/api-approval — 审批契约
3
+ *
4
+ * 模型发起、需要批准的操作以审批对象的形式出现:发起方发起并等结算,审批对象推给在场的审批面,能批的人应答,结果回给
5
+ * 发起方。审批只有一种,发起方给出这一张谁能批(canAnswer),另标出只有 owner 能批的那种(要 owner 确认的)。本包只有
6
+ * 类型、服务描述符 `approvals` 与卡片清洗函数 `sanitizeForCard`、`clipForCard`;实现见 @aalis/plugin-session-server。
7
+ *
8
+ * 三类使用方:
9
+ * - 发起方(执行守卫):`request(req, signal)` 发起一个审批并等它结算,拒绝时结果里带着回给模型的话;
10
+ * - 审批面(WebUI、终端、IM 私聊与群窗口):经绑定门面 `registerSurface(name)` 登记,随激活撤回。每个客户端 `attend`
11
+ * 一次,在场上报与应答都经它,审计里的审批面取登记身份,不取调用方自报。推给谁、算谁在场、认谁的应答是一条规则:
12
+ * 客户端的身份能批这一张(canAnswer),且它的接入覆盖这张审批({@link AttendScope});
13
+ * - 展示方:`list()` 与 `approval:created`、`approval:settled` 事件。
14
+ */
15
+ import { defineService } from '@aalis/core';
16
+ import { t } from './texts.js';
17
+ export const approvals = defineService('approvals', port => {
18
+ const surfaces = port.registrar({
19
+ key: entry => entry.name,
20
+ register: (service, entry) => {
21
+ const binding = service.attachSurface(entry.name, port.id);
22
+ entry.binding = binding;
23
+ return () => {
24
+ if (entry.binding === binding)
25
+ entry.binding = undefined;
26
+ binding.detach();
27
+ };
28
+ },
29
+ });
30
+ /** 客户端的句柄转到当前提供者的登记上;提供者换了,下一次调用时在新的登记上重新接入 */
31
+ function attend(entry, who, sink, scope) {
32
+ let left = false;
33
+ let current;
34
+ const bound = () => {
35
+ const binding = entry.binding;
36
+ if (left || !binding)
37
+ return undefined;
38
+ if (current?.binding !== binding)
39
+ current = { binding, attendance: binding.attend(who, sink, scope) };
40
+ return current.attendance;
41
+ };
42
+ bound();
43
+ return {
44
+ presence: active => bound()?.presence(active),
45
+ undelivered: approvalId => bound()?.undelivered(approvalId),
46
+ present: () => bound()?.present() ?? false,
47
+ respond: answer => bound()?.respond(answer) ?? { ok: false, refusal: 'unavailable', message: t('serviceAbsent') },
48
+ leave() {
49
+ left = true;
50
+ current?.attendance.leave();
51
+ current = undefined;
52
+ },
53
+ };
54
+ }
55
+ return Object.assign(Object.create(port.provider), {
56
+ registerSurface: (name) => {
57
+ const entry = { name };
58
+ surfaces.add(entry);
59
+ return { name, attend: (who, sink, scope) => attend(entry, who, sink, scope) };
60
+ },
61
+ });
62
+ });
63
+ // ════════════════════════════════════════════════════════════
64
+ // 卡片清洗
65
+ // ════════════════════════════════════════════════════════════
66
+ /** 控制符(C0、DEL、C1;ANSI 转义序列以 ESC 或 CSI 开头),换行与制表符除外 */
67
+ const CONTROL = /[\p{Cc}]/gu;
68
+ /** 看不见或会改变显示顺序的字符:格式符(双向控制、零宽字符、软连字符、标签字符等)、行与段分隔符、落单的代理项 */
69
+ const INVISIBLE = /[\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/gu;
70
+ const hex = (char, width) => (char.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(width, '0');
71
+ /**
72
+ * 审批卡显示事实之前的清洗:控制符(含 ANSI 转义序列的 ESC)转义成 `\x1B` 的样子显示;双向控制符、零宽字符等
73
+ * 看不见的字符换成 `⟨U+202E⟩` 的样子。换行与制表符保留。原文里的每个字符都显示出来,不隐藏、不改变顺序。
74
+ */
75
+ export function sanitizeForCard(text) {
76
+ return text
77
+ .replace(CONTROL, char => (char === '\n' || char === '\t' ? char : `\\x${hex(char, 2)}`))
78
+ .replace(INVISIBLE, char => `⟨U+${hex(char, 4)}⟩`);
79
+ }
80
+ /**
81
+ * 清洗(同 {@link sanitizeForCard})并截断:原文超过 limit 个字符时保留开头与结尾共 limit 个字符,交出两段与中间省略的
82
+ * 字数,显示方在两段之间按读者语言写明省略了多少。截断按原文的字符算,不会切开转义或占位符。
83
+ */
84
+ export function clipForCard(text, limit) {
85
+ const chars = [...text];
86
+ if (chars.length <= limit)
87
+ return { head: sanitizeForCard(text), omitted: 0, tail: '' };
88
+ const head = Math.ceil(limit / 2);
89
+ const tail = limit - head;
90
+ return {
91
+ head: sanitizeForCard(chars.slice(0, head).join('')),
92
+ omitted: chars.length - limit,
93
+ tail: sanitizeForCard(chars.slice(chars.length - tail).join('')),
94
+ };
95
+ }
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ declare const _default: {
2
+ readonly serviceAbsent: "The approval service is absent";
3
+ };
4
+ export default _default;
5
+ //# sourceMappingURL=en.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":";;;AAGA,wBAEG"}
@@ -0,0 +1,5 @@
1
+ import { defineLocale } from '@aalis/schema-i18n';
2
+ export default defineLocale()({
3
+ serviceAbsent: 'The approval service is absent',
4
+ });
5
+ //# sourceMappingURL=en.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,eAAe,YAAY,EAAU,CAAC;IACpC,aAAa,EAAE,gCAAgC;CAChD,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ declare const SOURCE: {
2
+ readonly serviceAbsent: "审批服务不在";
3
+ };
4
+ export type Source = typeof SOURCE;
5
+ export declare const t: import("@aalis/schema-i18n").TextFactory<{
6
+ readonly serviceAbsent: "审批服务不在";
7
+ }>;
8
+ export {};
9
+ //# sourceMappingURL=texts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"texts.d.ts","sourceRoot":"","sources":["../src/texts.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM;;CAEF,CAAC;AACX,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC;AAEnC,eAAO,MAAM,CAAC;;EAAiG,CAAC"}
package/dist/texts.js ADDED
@@ -0,0 +1,8 @@
1
+ // 本包自己的原文字典:审批面的句柄在提供者不在场时回的应答被拒原因
2
+ import { defineTexts } from '@aalis/schema-i18n';
3
+ import en from './locales/en.js';
4
+ const SOURCE = {
5
+ serviceAbsent: '审批服务不在',
6
+ };
7
+ export const t = defineTexts({ ns: '@aalis/api-approval', language: 'zh-CN', source: SOURCE, locales: { en } });
8
+ //# sourceMappingURL=texts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"texts.js","sourceRoot":"","sources":["../src/texts.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAEjC,MAAM,MAAM,GAAG;IACb,aAAa,EAAE,QAAQ;CACf,CAAC;AAGX,MAAM,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,EAAE,EAAE,EAAE,qBAAqB,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@aalis/api-approval",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/AalisLabs/Aalis.git",
8
+ "directory": "packages/api-approval"
9
+ },
10
+ "author": "Ace Nyan <ace@acenyan.com>",
11
+ "bugs": {
12
+ "url": "https://github.com/AalisLabs/Aalis/issues"
13
+ },
14
+ "homepage": "https://github.com/AalisLabs/Aalis#readme",
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "dependencies": {
22
+ "@aalis/schema-i18n": ">=0.1.0 <1.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "typescript": "^5.7.0",
27
+ "@aalis/core": "0.21.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@aalis/core": ">=0.21.0 <1.0.0"
31
+ },
32
+ "keywords": [
33
+ "aalis",
34
+ "aalis-api"
35
+ ],
36
+ "description": "审批契约:approvals 服务描述符(发起、应答、结算、审批面登记与在场)、审批对象的类型与卡片清洗函数",
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "dev": "tsc --watch"
40
+ }
6
41
  }