dsh-lark-agent 0.1.2 → 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/README.md +6 -2
- package/dist/bot-menu.d.ts +1 -0
- package/dist/bot-menu.js +2 -0
- package/dist/bot-service.js +4 -2
- package/dist/bridge.d.ts +42 -1
- package/dist/bridge.js +340 -18
- package/dist/card-actions.js +1 -1
- package/dist/client.cjs +3 -3
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -2
- package/dist/dsh-runtime.d.ts +75 -2
- package/dist/dsh-runtime.js +346 -23
- package/dist/interaction-cards.d.ts +23 -0
- package/dist/interaction-cards.js +119 -0
- package/dist/lark-cli.d.ts +24 -0
- package/dist/lark-cli.js +92 -4
- package/dist/live-reply.d.ts +87 -0
- package/dist/live-reply.js +389 -0
- package/dist/onboarding/controller.js +9 -7
- package/dist/onboarding/lark-platform.js +5 -5
- package/dist/permission-controls.d.ts +13 -0
- package/dist/permission-controls.js +26 -0
- package/dist/permission-preferences.d.ts +26 -0
- package/dist/permission-preferences.js +169 -0
- package/dist/presentation.d.ts +7 -0
- package/dist/replies.d.ts +5 -0
- package/dist/replies.js +9 -1
- package/dist/rich-scopes.d.ts +4 -0
- package/dist/rich-scopes.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { chmod, lstat, mkdir, open, rename, unlink } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
/** Safe for chat replies: no identity keys, paths or I/O details. */
|
|
6
|
+
export class PermissionPreferencesError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code) {
|
|
9
|
+
super({
|
|
10
|
+
INVALID_INPUT: '权限设置参数无效,请重新打开权限模式后选择。',
|
|
11
|
+
CAPACITY: '权限设置数量已达到上限,已有设置会保留。',
|
|
12
|
+
CORRUPT: '权限设置无法安全读取,请管理员检查本地状态文件;已有记录未被清空。',
|
|
13
|
+
WRITE_FAILED: '权限设置未能保存,请稍后重试。',
|
|
14
|
+
UNAVAILABLE: '权限设置的保存结果未能确认,请管理员检查后重新启动。',
|
|
15
|
+
}[code]);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = 'PermissionPreferencesError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const FILE_NAME = 'permission-preferences.json';
|
|
21
|
+
const MAX_ENTRIES = 10_000;
|
|
22
|
+
const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
const PRESET = /^[a-z][a-z0-9-]{0,39}$/;
|
|
24
|
+
const KEY = (value) => typeof value === 'string' && value.length > 0 && value.length <= 512
|
|
25
|
+
&& !/[\u0000-\u001f\u007f]/.test(value);
|
|
26
|
+
function snapshot(context) {
|
|
27
|
+
if (!context || typeof context !== 'object' || !KEY(context.ownerKey) || !KEY(context.conversationKey)) {
|
|
28
|
+
throw new PermissionPreferencesError('INVALID_INPUT');
|
|
29
|
+
}
|
|
30
|
+
return { ownerKey: context.ownerKey, conversationKey: context.conversationKey };
|
|
31
|
+
}
|
|
32
|
+
function parse(value) {
|
|
33
|
+
const input = value;
|
|
34
|
+
if (!input || typeof input !== 'object' || Object.keys(input).some(key => key !== 'version' && key !== 'entries')
|
|
35
|
+
|| input.version !== 1 || !Array.isArray(input.entries) || input.entries.length > MAX_ENTRIES)
|
|
36
|
+
throw new Error();
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const entries = input.entries.map((raw) => {
|
|
39
|
+
const row = raw;
|
|
40
|
+
if (!row || typeof row !== 'object' || Array.isArray(row) || Object.keys(row).some(key => !['ownerKey', 'conversationKey', 'preset'].includes(key))
|
|
41
|
+
|| typeof row.preset !== 'string' || !PRESET.test(row.preset))
|
|
42
|
+
throw new Error();
|
|
43
|
+
const context = snapshot(row);
|
|
44
|
+
const identity = JSON.stringify([context.ownerKey, context.conversationKey]);
|
|
45
|
+
if (seen.has(identity))
|
|
46
|
+
throw new Error();
|
|
47
|
+
seen.add(identity);
|
|
48
|
+
return { ...context, preset: row.preset };
|
|
49
|
+
});
|
|
50
|
+
return { version: 1, entries };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The permission preset a private chat chose in Feishu. It applies to that
|
|
54
|
+
* chat's sessions only; the host's own default for other sessions is untouched.
|
|
55
|
+
* One bot process owns this store under its state-directory lock.
|
|
56
|
+
*/
|
|
57
|
+
export class PermissionPreferencesStore {
|
|
58
|
+
directory;
|
|
59
|
+
document = { version: 1, entries: [] };
|
|
60
|
+
tail = Promise.resolve();
|
|
61
|
+
uncertain = false;
|
|
62
|
+
constructor(directory) {
|
|
63
|
+
this.directory = directory;
|
|
64
|
+
}
|
|
65
|
+
static async open(directory) {
|
|
66
|
+
if (typeof directory !== 'string' || !directory.trim() || directory.includes('\0'))
|
|
67
|
+
throw new PermissionPreferencesError('INVALID_INPUT');
|
|
68
|
+
const store = new PermissionPreferencesStore(resolve(directory));
|
|
69
|
+
try {
|
|
70
|
+
await mkdir(store.directory, { recursive: true, mode: 0o700 });
|
|
71
|
+
const info = await lstat(store.directory);
|
|
72
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
73
|
+
throw new Error();
|
|
74
|
+
await chmod(store.directory, 0o700);
|
|
75
|
+
const path = join(store.directory, FILE_NAME);
|
|
76
|
+
let file;
|
|
77
|
+
try {
|
|
78
|
+
file = await lstat(path);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code === 'ENOENT')
|
|
82
|
+
return store;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (!file.isFile() || file.isSymbolicLink() || file.size > MAX_FILE_BYTES)
|
|
86
|
+
throw new Error();
|
|
87
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
88
|
+
try {
|
|
89
|
+
const actual = await handle.stat();
|
|
90
|
+
if (!actual.isFile() || actual.dev !== file.dev || actual.ino !== file.ino || actual.size > MAX_FILE_BYTES)
|
|
91
|
+
throw new Error();
|
|
92
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(await handle.readFile());
|
|
93
|
+
if (Buffer.byteLength(text) !== actual.size)
|
|
94
|
+
throw new Error();
|
|
95
|
+
store.document = parse(JSON.parse(text));
|
|
96
|
+
await handle.chmod(0o600);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await handle.close();
|
|
100
|
+
}
|
|
101
|
+
return store;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new PermissionPreferencesError('CORRUPT');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async get(context) {
|
|
108
|
+
const identity = snapshot(context);
|
|
109
|
+
await this.tail;
|
|
110
|
+
if (this.uncertain)
|
|
111
|
+
throw new PermissionPreferencesError('UNAVAILABLE');
|
|
112
|
+
return this.document.entries.find(entry => entry.ownerKey === identity.ownerKey && entry.conversationKey === identity.conversationKey)?.preset;
|
|
113
|
+
}
|
|
114
|
+
async set(context, preset) {
|
|
115
|
+
const identity = snapshot(context);
|
|
116
|
+
if (typeof preset !== 'string' || !PRESET.test(preset))
|
|
117
|
+
throw new PermissionPreferencesError('INVALID_INPUT');
|
|
118
|
+
const job = this.tail.then(async () => {
|
|
119
|
+
if (this.uncertain)
|
|
120
|
+
throw new PermissionPreferencesError('UNAVAILABLE');
|
|
121
|
+
const previous = this.document.entries.find(entry => entry.ownerKey === identity.ownerKey && entry.conversationKey === identity.conversationKey);
|
|
122
|
+
if (previous?.preset === preset)
|
|
123
|
+
return;
|
|
124
|
+
if (!previous && this.document.entries.length >= MAX_ENTRIES)
|
|
125
|
+
throw new PermissionPreferencesError('CAPACITY');
|
|
126
|
+
const value = { ...identity, preset };
|
|
127
|
+
const entries = previous ? this.document.entries.map(entry => entry === previous ? value : entry) : [...this.document.entries, value];
|
|
128
|
+
const draft = { version: 1, entries };
|
|
129
|
+
await this.persist(draft);
|
|
130
|
+
this.document = draft;
|
|
131
|
+
});
|
|
132
|
+
this.tail = job.catch(() => undefined);
|
|
133
|
+
return job;
|
|
134
|
+
}
|
|
135
|
+
async persist(document) {
|
|
136
|
+
const content = JSON.stringify(document) + '\n';
|
|
137
|
+
if (Buffer.byteLength(content) > MAX_FILE_BYTES)
|
|
138
|
+
throw new PermissionPreferencesError('CAPACITY');
|
|
139
|
+
const temporary = join(this.directory, `permission-preferences.${randomUUID()}.tmp`);
|
|
140
|
+
let publicationAttempted = false;
|
|
141
|
+
try {
|
|
142
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
143
|
+
try {
|
|
144
|
+
await handle.writeFile(content, 'utf8');
|
|
145
|
+
await handle.sync();
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await handle.close();
|
|
149
|
+
}
|
|
150
|
+
// A rejected rename may leave publication uncertain; never overwrite from stale memory afterwards.
|
|
151
|
+
publicationAttempted = true;
|
|
152
|
+
await rename(temporary, join(this.directory, FILE_NAME));
|
|
153
|
+
const directory = await open(this.directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
154
|
+
try {
|
|
155
|
+
await directory.sync();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await directory.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
if (publicationAttempted)
|
|
163
|
+
this.uncertain = true;
|
|
164
|
+
await unlink(temporary).catch(() => undefined);
|
|
165
|
+
throw new PermissionPreferencesError(publicationAttempted ? 'UNAVAILABLE' : 'WRITE_FAILED');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=permission-preferences.js.map
|
package/dist/presentation.d.ts
CHANGED
|
@@ -55,9 +55,16 @@ export type PresentationDraft = DraftBase & ({
|
|
|
55
55
|
kind: 'form';
|
|
56
56
|
content: FormContent;
|
|
57
57
|
});
|
|
58
|
+
/** A file the host's own delivery tool presented; the path is absolute and not yet validated for upload. */
|
|
59
|
+
export interface DeliveredFile {
|
|
60
|
+
path: string;
|
|
61
|
+
description: string;
|
|
62
|
+
}
|
|
58
63
|
export interface TurnResult {
|
|
59
64
|
text: string;
|
|
60
65
|
artifacts: PresentationDraft[];
|
|
66
|
+
reasoning?: string;
|
|
67
|
+
files?: DeliveredFile[];
|
|
61
68
|
}
|
|
62
69
|
export declare const PRESENT_TOOL = "lark_present";
|
|
63
70
|
export declare const PRESENTATION_GUIDE = "\u9700\u8981\u56FE\u8868\u3001\u6D41\u7A0B\u56FE\u3001\u957F\u62A5\u544A\u6216\u6536\u96C6\u9009\u9879\u65F6\uFF0C\u53EF\u4EE5\u8C03\u7528 lark_present\u3002\u666E\u901A\u804A\u5929\u548C\u4EE3\u7801\u89E3\u91CA\u76F4\u63A5\u7528\u6587\u5B57\u3002\u5DE5\u5177\u53EA\u51C6\u5907\u5F53\u524D\u804A\u5929\u7684\u5C55\u793A\u5185\u5BB9\uFF0C\u4E0D\u6267\u884C HTML/JavaScript\uFF0C\u4E0D\u8BFB\u53D6\u6587\u4EF6\uFF0C\u4E0D\u6269\u5927\u4EFB\u4F55\u4EBA\u7684\u6743\u9650\u3002\u56FE\u8868\u6570\u636E\u5FC5\u987B\u6765\u81EA\u7528\u6237\u6216\u5DF2\u6838\u5B9E\u7684\u8D44\u6599\uFF1B\u793A\u4F8B\u6570\u636E\u5FC5\u987B\u5728\u6807\u9898\u6216\u6458\u8981\u6807\u660E\u201C\u793A\u4F8B\u201D\u3002\u8868\u5355\u53EA\u6536\u96C6\u5B8C\u6210\u5F53\u524D\u4EFB\u52A1\u5FC5\u9700\u7684\u666E\u901A\u4FE1\u606F\uFF0C\u4E0D\u7D22\u53D6\u5BC6\u7801\u3001\u5BC6\u94A5\u6216\u5176\u4ED6\u654F\u611F\u51ED\u636E\u3002\u4E0D\u8981\u91CD\u590D\u8F93\u51FA\u5DF2\u63D0\u4EA4\u5DE5\u5177\u7684\u6574\u4EFD\u5185\u5BB9\uFF0C\u4E5F\u4E0D\u8981\u58F0\u79F0\u5DF2\u521B\u5EFA\u4E91\u6587\u6863\u6216\u94FE\u63A5\uFF1B\u4EE5\u7CFB\u7EDF\u6700\u7EC8\u5C55\u793A\u7ED3\u679C\u4E3A\u51C6\u3002\u4FEE\u6539\u5DF2\u5C55\u793A\u4F5C\u54C1\u65F6\uFF0C\u53EA\u4F7F\u7528\u5F53\u524D\u804A\u5929\u63D0\u4F9B\u7684\u4F5C\u54C1 ID \u4F5C\u4E3A replaces\uFF0C\u4FDD\u7559\u539F\u7248\u3002";
|
package/dist/replies.d.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface ReplyPanel {
|
|
|
41
41
|
* Text messages understand native mentions even inside Markdown code fences.
|
|
42
42
|
*/
|
|
43
43
|
export declare function safePlainText(text: string): string;
|
|
44
|
+
/** Model text for a card markdown element, with platform extensions made inert. */
|
|
45
|
+
export declare function cardMarkdown(text: string): string;
|
|
46
|
+
/** Top-level blocks split at blank lines outside code fences, for cutting long text without breaking Markdown. */
|
|
47
|
+
export declare function markdownBlocks(text: string): string[];
|
|
48
|
+
export declare function plainLabel(text: string, limit: number): string;
|
|
44
49
|
/** Trusted application panels only. Model strings never choose components or callback values. */
|
|
45
50
|
export declare function renderPanel(panel: ReplyPanel): ReplyPayload;
|
|
46
51
|
/** Select presentation without asking the model to generate arbitrary card JSON. */
|
package/dist/replies.js
CHANGED
|
@@ -130,7 +130,15 @@ function markdown(text) {
|
|
|
130
130
|
blocks.push(block);
|
|
131
131
|
return { text: output, balanced: !fence, blocks };
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
/** Model text for a card markdown element, with platform extensions made inert. */
|
|
134
|
+
export function cardMarkdown(text) {
|
|
135
|
+
return markdown(text).text;
|
|
136
|
+
}
|
|
137
|
+
/** Top-level blocks split at blank lines outside code fences, for cutting long text without breaking Markdown. */
|
|
138
|
+
export function markdownBlocks(text) {
|
|
139
|
+
return markdown(text).blocks;
|
|
140
|
+
}
|
|
141
|
+
export function plainLabel(text, limit) {
|
|
134
142
|
return pointSlice(text.replace(/<[^>]*>/g, '').replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
|
|
135
143
|
.replace(/!?(\[([^\]]+)\])\([^)]*\)/g, '$2').replace(/[`*#_~]/g, '').replace(/\s+/g, ' ').trim(), limit);
|
|
136
144
|
}
|
package/dist/rich-scopes.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
/** Default document/board capabilities, authorized at registration. Bot-only; no broad drive access. */
|
|
2
2
|
export declare const DOCUMENT_SCOPES: readonly ["application:application:self_manage", "docx:document:create", "docx:document:write_only", "docx:document:readonly", "board:whiteboard:node:create", "board:whiteboard:node:read", "docs:permission.setting:write_only", "docs:permission.setting:read", "docs:permission.member:create", "docs:permission.member:retrieve", "im:resource:upload"];
|
|
3
|
+
/** Lets a harness-mode private chat stream its reply, reasoning and tool progress into one card. */
|
|
4
|
+
export declare const STREAMING_SCOPE = "cardkit:card:write";
|
|
3
5
|
export declare function documentScopesReady(scopes: readonly string[], diagram?: boolean): boolean;
|
|
6
|
+
/** Everything the owner-confirmed "default permissions" step grants: documents, boards and streaming replies. */
|
|
7
|
+
export declare function defaultScopesReady(scopes: readonly string[]): boolean;
|
package/dist/rich-scopes.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export const DOCUMENT_SCOPES = ['application:application:self_manage', 'docx:document:create', 'docx:document:write_only', 'docx:document:readonly',
|
|
3
3
|
'board:whiteboard:node:create', 'board:whiteboard:node:read', 'docs:permission.setting:write_only',
|
|
4
4
|
'docs:permission.setting:read', 'docs:permission.member:create', 'docs:permission.member:retrieve', 'im:resource:upload'];
|
|
5
|
+
/** Lets a harness-mode private chat stream its reply, reasoning and tool progress into one card. */
|
|
6
|
+
export const STREAMING_SCOPE = 'cardkit:card:write';
|
|
5
7
|
export function documentScopesReady(scopes, diagram = true) {
|
|
6
8
|
const granted = new Set(scopes);
|
|
7
9
|
const groups = [
|
|
@@ -12,4 +14,8 @@ export function documentScopesReady(scopes, diagram = true) {
|
|
|
12
14
|
];
|
|
13
15
|
return groups.every(group => group.some(scope => granted.has(scope)));
|
|
14
16
|
}
|
|
17
|
+
/** Everything the owner-confirmed "default permissions" step grants: documents, boards and streaming replies. */
|
|
18
|
+
export function defaultScopesReady(scopes) {
|
|
19
|
+
return documentScopesReady(scopes) && scopes.includes(STREAMING_SCOPE);
|
|
20
|
+
}
|
|
15
21
|
//# sourceMappingURL=rich-scopes.js.map
|