@coze-arch/cli 0.0.13 → 0.0.14-alpha.c52ee4
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/lib/__templates__/expo/AGENTS.md +15 -7
- package/lib/__templates__/expo/README.md +15 -7
- package/lib/__templates__/expo/client/eslint.config.mjs +3 -0
- package/lib/__templates__/expo/eslint-plugins/expo/index.js +9 -0
- package/lib/__templates__/expo/eslint-plugins/expo/rule.js +105 -0
- package/lib/__templates__/expo/eslint-plugins/expo/tech.md +108 -0
- package/lib/__templates__/nextjs/AGENTS.md +9 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +15 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +150 -0
- package/lib/__templates__/pi-agent/README.md +155 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +273 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
- package/lib/__templates__/pi-agent/package.json +52 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +7840 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
- package/lib/__templates__/pi-agent/src/agent.ts +367 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
- package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
- package/lib/__templates__/pi-agent/src/config.ts +596 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
- package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
- package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
- package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
- package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
- package/lib/__templates__/pi-agent/src/index.ts +123 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +292 -0
- package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
- package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
- package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
- package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
- package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
- package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
- package/lib/__templates__/pi-agent/tsconfig.json +20 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
- package/lib/__templates__/taro/server/package.json +0 -2
- package/lib/__templates__/taro/src/presets/dev-debug.ts +2 -2
- package/lib/__templates__/templates.json +24 -0
- package/lib/__templates__/vite/AGENTS.md +5 -0
- package/lib/cli.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createFeishuChannel } from "../src/channels/feishu/index.js";
|
|
4
|
+
|
|
5
|
+
test("feishu channel adds and removes thinking reaction around replies", async () => {
|
|
6
|
+
const operations: string[] = [];
|
|
7
|
+
let nextReactionId = 0;
|
|
8
|
+
|
|
9
|
+
const channel = createFeishuChannel(
|
|
10
|
+
{
|
|
11
|
+
thinkingReaction: {
|
|
12
|
+
enabled: true,
|
|
13
|
+
emojiType: "OneSecond"
|
|
14
|
+
},
|
|
15
|
+
transport: {
|
|
16
|
+
addReaction({ messageId, emojiType }) {
|
|
17
|
+
nextReactionId += 1;
|
|
18
|
+
operations.push(`add:${messageId}:${emojiType}`);
|
|
19
|
+
return {
|
|
20
|
+
reactionId: `reaction-${nextReactionId}`,
|
|
21
|
+
emojiType
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
removeReaction({ messageId, reactionId, emojiType }) {
|
|
25
|
+
operations.push(`remove:${messageId}:${reactionId}:${emojiType}`);
|
|
26
|
+
},
|
|
27
|
+
send(message) {
|
|
28
|
+
operations.push(`send:${message.replyToMessageId}:${message.text}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
async onMessage() {
|
|
34
|
+
operations.push("handler:start");
|
|
35
|
+
await Promise.resolve();
|
|
36
|
+
operations.push("handler:end");
|
|
37
|
+
return {
|
|
38
|
+
text: "reply text"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const result = await channel.simulateIncomingText({
|
|
45
|
+
text: "hello",
|
|
46
|
+
senderId: "user-1",
|
|
47
|
+
conversationId: "dm-1",
|
|
48
|
+
isDirectMessage: true
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.equal(result.handled, true);
|
|
52
|
+
assert.equal(result.reply?.text, "reply text");
|
|
53
|
+
assert.deepEqual(operations, [
|
|
54
|
+
`add:${result.message?.messageId}:OneSecond`,
|
|
55
|
+
"handler:start",
|
|
56
|
+
"handler:end",
|
|
57
|
+
`remove:${result.message?.messageId}:reaction-1:OneSecond`,
|
|
58
|
+
`send:${result.message?.messageId}:reply text`
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("feishu channel defaults thinking reaction emoji to OneSecond", async () => {
|
|
63
|
+
const operations: string[] = [];
|
|
64
|
+
|
|
65
|
+
const channel = createFeishuChannel(
|
|
66
|
+
{
|
|
67
|
+
transport: {
|
|
68
|
+
addReaction({ messageId, emojiType }) {
|
|
69
|
+
operations.push(`add:${messageId}:${emojiType}`);
|
|
70
|
+
return {
|
|
71
|
+
reactionId: "reaction-1",
|
|
72
|
+
emojiType
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
removeReaction({ messageId, reactionId, emojiType }) {
|
|
76
|
+
operations.push(`remove:${messageId}:${reactionId}:${emojiType}`);
|
|
77
|
+
},
|
|
78
|
+
send(message) {
|
|
79
|
+
operations.push(`send:${message.replyToMessageId}:${message.text}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
onMessage() {
|
|
85
|
+
return {
|
|
86
|
+
text: "reply text"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const result = await channel.simulateIncomingText({
|
|
93
|
+
text: "hello",
|
|
94
|
+
senderId: "user-default",
|
|
95
|
+
conversationId: "dm-default",
|
|
96
|
+
isDirectMessage: true
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.equal(result.handled, true);
|
|
100
|
+
assert.deepEqual(operations, [
|
|
101
|
+
`add:${result.message?.messageId}:OneSecond`,
|
|
102
|
+
`remove:${result.message?.messageId}:reaction-1:OneSecond`,
|
|
103
|
+
`send:${result.message?.messageId}:reply text`
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("feishu channel still replies when thinking reaction fails", async () => {
|
|
108
|
+
const operations: string[] = [];
|
|
109
|
+
|
|
110
|
+
const channel = createFeishuChannel(
|
|
111
|
+
{
|
|
112
|
+
thinkingReaction: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
emojiType: "OneSecond"
|
|
115
|
+
},
|
|
116
|
+
transport: {
|
|
117
|
+
addReaction() {
|
|
118
|
+
operations.push("add-failed");
|
|
119
|
+
throw new Error("reaction unavailable");
|
|
120
|
+
},
|
|
121
|
+
send(message) {
|
|
122
|
+
operations.push(`send:${message.replyToMessageId}:${message.text}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
onMessage() {
|
|
128
|
+
operations.push("handler");
|
|
129
|
+
return {
|
|
130
|
+
text: "reply after failure"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const result = await channel.simulateIncomingText({
|
|
137
|
+
text: "hello",
|
|
138
|
+
senderId: "user-2",
|
|
139
|
+
conversationId: "dm-2",
|
|
140
|
+
isDirectMessage: true
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert.equal(result.handled, true);
|
|
144
|
+
assert.deepEqual(operations, [
|
|
145
|
+
"add-failed",
|
|
146
|
+
"handler",
|
|
147
|
+
`send:${result.message?.messageId}:reply after failure`
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { mergeStreamingText } from "../src/channels/feishu/streaming-card.js";
|
|
4
|
+
|
|
5
|
+
test("mergeStreamingText prefers progressive full-text updates", () => {
|
|
6
|
+
assert.equal(mergeStreamingText("", "hel"), "hel");
|
|
7
|
+
assert.equal(mergeStreamingText("hel", "hello"), "hello");
|
|
8
|
+
assert.equal(mergeStreamingText("hello", "hello world"), "hello world");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("mergeStreamingText merges overlapping chunks without duplicating suffixes", () => {
|
|
12
|
+
assert.equal(mergeStreamingText("hello", "lo world"), "hello world");
|
|
13
|
+
assert.equal(mergeStreamingText("abc", "bcdef"), "abcdef");
|
|
14
|
+
});
|
|
15
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { SessionStore } from "../src/session-store.js";
|
|
7
|
+
|
|
8
|
+
test("SessionStore persists index + transcript header and can reload", () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "pi-bot-session-store-"));
|
|
10
|
+
try {
|
|
11
|
+
const store1 = new SessionStore(root);
|
|
12
|
+
store1.load();
|
|
13
|
+
|
|
14
|
+
const key = "dashboard:dm:dashboard-user";
|
|
15
|
+
const record1 = store1.ensureSession(key);
|
|
16
|
+
|
|
17
|
+
assert.equal(record1.sessionKey, key);
|
|
18
|
+
assert.ok(record1.sessionId);
|
|
19
|
+
assert.ok(record1.sessionFile.endsWith(".jsonl"));
|
|
20
|
+
assert.ok(existsSync(record1.sessionFile));
|
|
21
|
+
|
|
22
|
+
const firstLine = readFileSync(record1.sessionFile, "utf-8").split(/\r?\n/)[0] ?? "";
|
|
23
|
+
const header = JSON.parse(firstLine) as { type?: string; id?: string };
|
|
24
|
+
assert.equal(header.type, "session");
|
|
25
|
+
assert.equal(header.id, record1.sessionId);
|
|
26
|
+
|
|
27
|
+
const store2 = new SessionStore(root);
|
|
28
|
+
store2.load();
|
|
29
|
+
assert.deepEqual(store2.listSessionKeys(), [key]);
|
|
30
|
+
const record2 = store2.ensureSession(key);
|
|
31
|
+
assert.equal(record2.sessionId, record1.sessionId);
|
|
32
|
+
assert.equal(record2.sessionFile, record1.sessionFile);
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(root, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("SessionStore reset archives prior transcript and rotates sessionId", () => {
|
|
39
|
+
const root = mkdtempSync(join(tmpdir(), "pi-bot-session-store-reset-"));
|
|
40
|
+
try {
|
|
41
|
+
const store = new SessionStore(root);
|
|
42
|
+
store.load();
|
|
43
|
+
|
|
44
|
+
const key = "feishu:dm:user-1";
|
|
45
|
+
const first = store.ensureSession(key);
|
|
46
|
+
assert.ok(existsSync(first.sessionFile));
|
|
47
|
+
|
|
48
|
+
const second = store.resetSession(key);
|
|
49
|
+
assert.notEqual(second.sessionId, first.sessionId);
|
|
50
|
+
assert.notEqual(second.sessionFile, first.sessionFile);
|
|
51
|
+
assert.ok(existsSync(second.sessionFile));
|
|
52
|
+
|
|
53
|
+
const sessionsDir = join(root, ".pi-bot", "sessions");
|
|
54
|
+
const archiveDir = join(sessionsDir, "archive");
|
|
55
|
+
// Old file should have been moved into archive (best-effort rename).
|
|
56
|
+
assert.ok(existsSync(archiveDir));
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(root, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createBotApp } from "../../src/index.js";
|
|
4
|
+
import { createMemoryConfigStore } from "../../src/dashboard/config-store.js";
|
|
5
|
+
|
|
6
|
+
async function readJson(url: string, init?: RequestInit): Promise<unknown> {
|
|
7
|
+
const res = await fetch(url, init);
|
|
8
|
+
assert.equal(res.ok, true, `${init?.method ?? "GET"} ${url} failed with ${res.status}`);
|
|
9
|
+
return (await res.json()) as unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function allocateFreePort(): Promise<number> {
|
|
13
|
+
return await new Promise<number>((resolve, reject) => {
|
|
14
|
+
const server = createServer();
|
|
15
|
+
server.once("error", reject);
|
|
16
|
+
server.listen(0, "127.0.0.1", () => {
|
|
17
|
+
const address = server.address();
|
|
18
|
+
if (!address || typeof address === "string") {
|
|
19
|
+
server.close(() => reject(new Error("Failed to allocate free port.")));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { port } = address;
|
|
23
|
+
server.close((error) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
reject(error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolve(port);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function run(): Promise<void> {
|
|
35
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
36
|
+
const previousDashboardPort = process.env.PI_BOT_DASHBOARD_PORT;
|
|
37
|
+
process.env.NODE_ENV = "production";
|
|
38
|
+
process.env.PI_BOT_DASHBOARD_PORT = String(await allocateFreePort());
|
|
39
|
+
|
|
40
|
+
const configStore = createMemoryConfigStore({
|
|
41
|
+
agents: {
|
|
42
|
+
defaults: {
|
|
43
|
+
model: {
|
|
44
|
+
primary: "coze/auto"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
models: {
|
|
49
|
+
providers: {
|
|
50
|
+
coze: {
|
|
51
|
+
models: [
|
|
52
|
+
{ id: "auto", name: "Auto" },
|
|
53
|
+
{ id: "glm-4.7", name: "GLM-4.7" }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
channels: {
|
|
59
|
+
feishu: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
requireMention: true,
|
|
62
|
+
appId: "app-id",
|
|
63
|
+
thinkingReaction: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
emojiType: "SMILE"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
wechat: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
requireMention: false,
|
|
71
|
+
implementation: "mock"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let app: Awaited<ReturnType<typeof createBotApp>> | null = null;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
app = await createBotApp(
|
|
80
|
+
{
|
|
81
|
+
appName: "smoke-bot",
|
|
82
|
+
agent: {
|
|
83
|
+
mode: "mock"
|
|
84
|
+
},
|
|
85
|
+
routing: {
|
|
86
|
+
feishuGroupRequireMention: true,
|
|
87
|
+
wechatGroupRequireMention: false
|
|
88
|
+
},
|
|
89
|
+
channels: {
|
|
90
|
+
feishu: { enabled: true },
|
|
91
|
+
wechat: { enabled: true }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
dashboardConfigStore: configStore
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await app.start();
|
|
100
|
+
|
|
101
|
+
const dashboardUrl = app.dashboard.getUrl();
|
|
102
|
+
const initialModels = (await readJson(`${dashboardUrl}/api/models`)) as {
|
|
103
|
+
defaultModel: string;
|
|
104
|
+
options: Array<{ value: string; label: string }>;
|
|
105
|
+
};
|
|
106
|
+
assert.equal(initialModels.defaultModel, "coze/auto");
|
|
107
|
+
assert.deepEqual(initialModels.options, [
|
|
108
|
+
{ value: "coze/auto", label: "coze / Auto" },
|
|
109
|
+
{ value: "coze/glm-4.7", label: "coze / GLM-4.7" }
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
await readJson(`${dashboardUrl}/api/models`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json"
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
models: {
|
|
119
|
+
defaultModel: "coze/glm-4.7"
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
});
|
|
123
|
+
const savedModels = (await readJson(`${dashboardUrl}/api/models`)) as { defaultModel: string };
|
|
124
|
+
assert.equal(savedModels.defaultModel, "coze/glm-4.7");
|
|
125
|
+
assert.equal(
|
|
126
|
+
(((configStore.snapshot().agents as { defaults?: { model?: { primary?: string } } }).defaults?.model?.primary) ?? ""),
|
|
127
|
+
"coze/glm-4.7"
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const initialChannels = (await readJson(`${dashboardUrl}/api/channels`)) as {
|
|
131
|
+
feishu: { enabled: boolean; thinkingReaction?: { enabled?: boolean; emojiType?: string } };
|
|
132
|
+
wechat: { enabled: boolean; implementation?: string };
|
|
133
|
+
routing: { feishuGroupRequireMention: boolean; wechatGroupRequireMention: boolean };
|
|
134
|
+
};
|
|
135
|
+
assert.equal(initialChannels.feishu.enabled, true);
|
|
136
|
+
assert.equal(initialChannels.feishu.thinkingReaction?.emojiType, "SMILE");
|
|
137
|
+
assert.equal(initialChannels.wechat.implementation, "mock");
|
|
138
|
+
|
|
139
|
+
await readJson(`${dashboardUrl}/api/channels`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/json"
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
channels: {
|
|
146
|
+
routing: {
|
|
147
|
+
feishuGroupRequireMention: false,
|
|
148
|
+
wechatGroupRequireMention: true
|
|
149
|
+
},
|
|
150
|
+
feishu: {
|
|
151
|
+
enabled: false,
|
|
152
|
+
requireMention: false,
|
|
153
|
+
appId: "updated-app-id",
|
|
154
|
+
domain: "",
|
|
155
|
+
encryptKey: "",
|
|
156
|
+
verificationToken: "",
|
|
157
|
+
appSecret: "",
|
|
158
|
+
thinkingReaction: {
|
|
159
|
+
enabled: false,
|
|
160
|
+
emojiType: "THINK"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
wechat: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
requireMention: true,
|
|
166
|
+
implementation: "mock"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
const savedChannels = (await readJson(`${dashboardUrl}/api/channels`)) as {
|
|
172
|
+
feishu: { enabled: boolean; appId?: string; thinkingReaction?: { enabled?: boolean; emojiType?: string } };
|
|
173
|
+
wechat: { requireMention?: boolean };
|
|
174
|
+
};
|
|
175
|
+
assert.equal(savedChannels.feishu.enabled, false);
|
|
176
|
+
assert.equal(savedChannels.feishu.appId, "updated-app-id");
|
|
177
|
+
assert.equal(savedChannels.feishu.thinkingReaction?.enabled, false);
|
|
178
|
+
assert.equal(savedChannels.wechat.requireMention, true);
|
|
179
|
+
|
|
180
|
+
const feishuResult = await app.channels.feishu?.simulateIncomingText({
|
|
181
|
+
text: "hello from feishu",
|
|
182
|
+
senderId: "feishu-user-1",
|
|
183
|
+
conversationId: "feishu-dm-1",
|
|
184
|
+
isDirectMessage: true
|
|
185
|
+
});
|
|
186
|
+
assert.ok(feishuResult);
|
|
187
|
+
assert.equal(feishuResult.handled, true);
|
|
188
|
+
assert.equal(feishuResult.reply?.text, "mock:feishu:dm:feishu-user-1: hello from feishu");
|
|
189
|
+
|
|
190
|
+
const duplicateFeishuEvent = {
|
|
191
|
+
message: {
|
|
192
|
+
message_id: "feishu-duplicate-1",
|
|
193
|
+
chat_id: "feishu-dm-duplicate",
|
|
194
|
+
chat_type: "p2p",
|
|
195
|
+
content: JSON.stringify({ text: "duplicate hello" })
|
|
196
|
+
},
|
|
197
|
+
sender: {
|
|
198
|
+
sender_id: {
|
|
199
|
+
open_id: "feishu-user-duplicate"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const firstDuplicateResult = await app.channels.feishu?.handleEvent(duplicateFeishuEvent);
|
|
205
|
+
assert.ok(firstDuplicateResult);
|
|
206
|
+
assert.equal(firstDuplicateResult.handled, true);
|
|
207
|
+
|
|
208
|
+
const secondDuplicateResult = await app.channels.feishu?.handleEvent(duplicateFeishuEvent);
|
|
209
|
+
assert.ok(secondDuplicateResult);
|
|
210
|
+
assert.equal(secondDuplicateResult.handled, false);
|
|
211
|
+
assert.equal(secondDuplicateResult.reason, "ignored");
|
|
212
|
+
|
|
213
|
+
const filteredGroupMessage = await app.channels.feishu?.simulateIncomingText({
|
|
214
|
+
text: "group message without mention",
|
|
215
|
+
senderId: "feishu-user-2",
|
|
216
|
+
conversationId: "feishu-group-1",
|
|
217
|
+
isDirectMessage: false,
|
|
218
|
+
mentions: []
|
|
219
|
+
});
|
|
220
|
+
assert.ok(filteredGroupMessage);
|
|
221
|
+
assert.equal(filteredGroupMessage.handled, false);
|
|
222
|
+
|
|
223
|
+
const handledGroupMessage = await app.channels.feishu?.simulateIncomingText({
|
|
224
|
+
text: "group message with mention",
|
|
225
|
+
senderId: "feishu-user-2",
|
|
226
|
+
conversationId: "feishu-group-1",
|
|
227
|
+
isDirectMessage: false,
|
|
228
|
+
mentions: [{ id: "bot" }]
|
|
229
|
+
});
|
|
230
|
+
assert.ok(handledGroupMessage);
|
|
231
|
+
assert.equal(handledGroupMessage.handled, true);
|
|
232
|
+
|
|
233
|
+
const wechatResult = await app.channels.wechat?.simulateIncomingText({
|
|
234
|
+
text: "hello from wechat",
|
|
235
|
+
senderId: "wechat-user-1",
|
|
236
|
+
conversationId: "wechat-dm-1",
|
|
237
|
+
isDirectMessage: true
|
|
238
|
+
});
|
|
239
|
+
assert.ok(wechatResult);
|
|
240
|
+
assert.equal(wechatResult.handled, true);
|
|
241
|
+
assert.equal(wechatResult.reply?.text, "mock:wechat:group:wechat-dm-1: hello from wechat");
|
|
242
|
+
|
|
243
|
+
const secondWechatResult = await app.channels.wechat?.simulateIncomingText({
|
|
244
|
+
text: "hello again from wechat",
|
|
245
|
+
senderId: "wechat-user-1",
|
|
246
|
+
conversationId: "wechat-dm-1",
|
|
247
|
+
isDirectMessage: true
|
|
248
|
+
});
|
|
249
|
+
assert.ok(secondWechatResult);
|
|
250
|
+
assert.equal(secondWechatResult.handled, true);
|
|
251
|
+
assert.equal(
|
|
252
|
+
secondWechatResult.reply?.text,
|
|
253
|
+
"mock:wechat:group:wechat-dm-1: hello again from wechat"
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
await app?.stop();
|
|
257
|
+
if (previousNodeEnv === undefined) {
|
|
258
|
+
delete process.env.NODE_ENV;
|
|
259
|
+
} else {
|
|
260
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
261
|
+
}
|
|
262
|
+
if (previousDashboardPort === undefined) {
|
|
263
|
+
delete process.env.PI_BOT_DASHBOARD_PORT;
|
|
264
|
+
} else {
|
|
265
|
+
process.env.PI_BOT_DASHBOARD_PORT = previousDashboardPort;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log("smoke test passed");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
run().catch((error: unknown) => {
|
|
273
|
+
console.error(error);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"baseUrl": "."
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*.ts",
|
|
17
|
+
"tests/**/*.ts",
|
|
18
|
+
"types/**/*.d.ts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
declare module "@larksuiteoapi/node-sdk" {
|
|
2
|
+
export enum AppType {
|
|
3
|
+
SelfBuild = "SelfBuild"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export enum Domain {
|
|
7
|
+
Feishu = "https://open.feishu.cn",
|
|
8
|
+
Lark = "https://open.larksuite.com"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export enum LoggerLevel {
|
|
12
|
+
warn = "warn"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClientOptions {
|
|
16
|
+
appId: string;
|
|
17
|
+
appSecret: string;
|
|
18
|
+
appType: AppType;
|
|
19
|
+
domain?: Domain | string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MessageReplyArgs {
|
|
23
|
+
path: {
|
|
24
|
+
message_id: string;
|
|
25
|
+
};
|
|
26
|
+
data: {
|
|
27
|
+
msg_type: string;
|
|
28
|
+
content: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MessageReactionCreateArgs {
|
|
33
|
+
path: {
|
|
34
|
+
message_id: string;
|
|
35
|
+
};
|
|
36
|
+
data: {
|
|
37
|
+
reaction_type: {
|
|
38
|
+
emoji_type: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MessageReactionDeleteArgs {
|
|
44
|
+
path: {
|
|
45
|
+
message_id: string;
|
|
46
|
+
reaction_id: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class Client {
|
|
51
|
+
constructor(options: ClientOptions);
|
|
52
|
+
request(args: {
|
|
53
|
+
method: string;
|
|
54
|
+
url: string;
|
|
55
|
+
data: Record<string, never>;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
bot?: {
|
|
58
|
+
open_id?: string;
|
|
59
|
+
};
|
|
60
|
+
data?: {
|
|
61
|
+
bot?: {
|
|
62
|
+
open_id?: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
im: {
|
|
67
|
+
message: {
|
|
68
|
+
reply(args: MessageReplyArgs): Promise<unknown>;
|
|
69
|
+
};
|
|
70
|
+
messageReaction: {
|
|
71
|
+
create(args: MessageReactionCreateArgs): Promise<{
|
|
72
|
+
code?: number;
|
|
73
|
+
msg?: string;
|
|
74
|
+
data?: {
|
|
75
|
+
reaction_id?: string;
|
|
76
|
+
reaction_type?: {
|
|
77
|
+
emoji_type?: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
}>;
|
|
81
|
+
delete(args: MessageReactionDeleteArgs): Promise<{
|
|
82
|
+
code?: number;
|
|
83
|
+
msg?: string;
|
|
84
|
+
data?: {
|
|
85
|
+
reaction_id?: string;
|
|
86
|
+
reaction_type?: {
|
|
87
|
+
emoji_type?: string;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class EventDispatcher {
|
|
96
|
+
constructor(options: {
|
|
97
|
+
encryptKey: string;
|
|
98
|
+
verificationToken: string;
|
|
99
|
+
});
|
|
100
|
+
register(handlers: Record<string, (data: unknown) => Promise<void> | void>): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class WSClient {
|
|
104
|
+
constructor(options: {
|
|
105
|
+
appId: string;
|
|
106
|
+
appSecret: string;
|
|
107
|
+
domain?: Domain | string;
|
|
108
|
+
loggerLevel?: LoggerLevel;
|
|
109
|
+
});
|
|
110
|
+
start(args: { eventDispatcher: EventDispatcher }): Promise<void>;
|
|
111
|
+
close(args?: { force?: boolean }): void;
|
|
112
|
+
}
|
|
113
|
+
}
|