@oyasmi/pipiclaw 0.6.2 → 0.6.3
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/dist/log.js +25 -22
- package/dist/runtime/bootstrap.js +14 -0
- package/dist/runtime/dingtalk.d.ts +6 -0
- package/dist/runtime/dingtalk.js +104 -7
- package/package.json +3 -6
package/dist/log.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
function color(style, text) {
|
|
3
|
+
return styleText(style, text);
|
|
4
|
+
}
|
|
2
5
|
function timestamp() {
|
|
3
6
|
const now = new Date();
|
|
4
7
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
@@ -50,91 +53,91 @@ function formatToolArgs(args) {
|
|
|
50
53
|
}
|
|
51
54
|
// User messages
|
|
52
55
|
export function logUserMessage(ctx, text) {
|
|
53
|
-
console.log(
|
|
56
|
+
console.log(color("green", `${timestamp()} ${formatContext(ctx)} ${text}`));
|
|
54
57
|
}
|
|
55
58
|
// Tool execution
|
|
56
59
|
export function logToolStart(ctx, toolName, label, args) {
|
|
57
60
|
const formattedArgs = formatToolArgs(args);
|
|
58
|
-
console.log(
|
|
61
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
|
|
59
62
|
if (formattedArgs) {
|
|
60
63
|
const indented = formattedArgs
|
|
61
64
|
.split("\n")
|
|
62
65
|
.map((line) => ` ${line}`)
|
|
63
66
|
.join("\n");
|
|
64
|
-
console.log(
|
|
67
|
+
console.log(color("dim", indented));
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
export function logToolSuccess(ctx, toolName, durationMs, result) {
|
|
68
71
|
const duration = (durationMs / 1000).toFixed(1);
|
|
69
|
-
console.log(
|
|
72
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
|
|
70
73
|
const truncated = truncate(result, 1000);
|
|
71
74
|
if (truncated) {
|
|
72
75
|
const indented = truncated
|
|
73
76
|
.split("\n")
|
|
74
77
|
.map((line) => ` ${line}`)
|
|
75
78
|
.join("\n");
|
|
76
|
-
console.log(
|
|
79
|
+
console.log(color("dim", indented));
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
export function logToolError(ctx, toolName, durationMs, error) {
|
|
80
83
|
const duration = (durationMs / 1000).toFixed(1);
|
|
81
|
-
console.log(
|
|
84
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
|
|
82
85
|
const truncated = truncate(error, 1000);
|
|
83
86
|
const indented = truncated
|
|
84
87
|
.split("\n")
|
|
85
88
|
.map((line) => ` ${line}`)
|
|
86
89
|
.join("\n");
|
|
87
|
-
console.log(
|
|
90
|
+
console.log(color("dim", indented));
|
|
88
91
|
}
|
|
89
92
|
// Response streaming
|
|
90
93
|
export function logResponseStart(ctx) {
|
|
91
|
-
console.log(
|
|
94
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} → Streaming response...`));
|
|
92
95
|
}
|
|
93
96
|
export function logThinking(ctx, thinking) {
|
|
94
|
-
console.log(
|
|
97
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
|
|
95
98
|
const truncated = truncate(thinking, 1000);
|
|
96
99
|
const indented = truncated
|
|
97
100
|
.split("\n")
|
|
98
101
|
.map((line) => ` ${line}`)
|
|
99
102
|
.join("\n");
|
|
100
|
-
console.log(
|
|
103
|
+
console.log(color("dim", indented));
|
|
101
104
|
}
|
|
102
105
|
export function logResponse(ctx, text) {
|
|
103
|
-
console.log(
|
|
106
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💬 Response`));
|
|
104
107
|
const truncated = truncate(text, 1000);
|
|
105
108
|
const indented = truncated
|
|
106
109
|
.split("\n")
|
|
107
110
|
.map((line) => ` ${line}`)
|
|
108
111
|
.join("\n");
|
|
109
|
-
console.log(
|
|
112
|
+
console.log(color("dim", indented));
|
|
110
113
|
}
|
|
111
114
|
// Control
|
|
112
115
|
export function logStopRequest(ctx) {
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(
|
|
116
|
+
console.log(color("green", `${timestamp()} ${formatContext(ctx)} stop`));
|
|
117
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
|
|
115
118
|
}
|
|
116
119
|
// System
|
|
117
120
|
export function logInfo(message) {
|
|
118
|
-
console.log(
|
|
121
|
+
console.log(color("blue", `${timestamp()} [system] ${message}`));
|
|
119
122
|
}
|
|
120
123
|
export function logWarning(message, details) {
|
|
121
|
-
console.log(
|
|
124
|
+
console.log(color("yellow", `${timestamp()} [system] ⚠ ${message}`));
|
|
122
125
|
if (details) {
|
|
123
126
|
const indented = details
|
|
124
127
|
.split("\n")
|
|
125
128
|
.map((line) => ` ${line}`)
|
|
126
129
|
.join("\n");
|
|
127
|
-
console.log(
|
|
130
|
+
console.log(color("dim", indented));
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
export function logAgentError(ctx, error) {
|
|
131
134
|
const context = ctx === "system" ? "[system]" : formatContext(ctx);
|
|
132
|
-
console.log(
|
|
135
|
+
console.log(color("yellow", `${timestamp()} ${context} ✗ Agent error`));
|
|
133
136
|
const indented = error
|
|
134
137
|
.split("\n")
|
|
135
138
|
.map((line) => ` ${line}`)
|
|
136
139
|
.join("\n");
|
|
137
|
-
console.log(
|
|
140
|
+
console.log(color("dim", indented));
|
|
138
141
|
}
|
|
139
142
|
// Usage summary
|
|
140
143
|
export function logUsageSummary(ctx, usage, contextTokens, contextWindow) {
|
|
@@ -164,8 +167,8 @@ export function logUsageSummary(ctx, usage, contextTokens, contextWindow) {
|
|
|
164
167
|
lines.push(`**Total: $${usage.cost.total.toFixed(4)}**`);
|
|
165
168
|
const summary = lines.join("\n");
|
|
166
169
|
// Log to console
|
|
167
|
-
console.log(
|
|
168
|
-
console.log(
|
|
170
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💰 Usage`));
|
|
171
|
+
console.log(color("dim", ` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
|
|
169
172
|
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
|
170
173
|
? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
|
|
171
174
|
: "") +
|
|
@@ -160,6 +160,15 @@ export class BootstrapExitError extends Error {
|
|
|
160
160
|
export function isBootstrapExitError(error) {
|
|
161
161
|
return error instanceof BootstrapExitError;
|
|
162
162
|
}
|
|
163
|
+
function readCliVersion() {
|
|
164
|
+
try {
|
|
165
|
+
const raw = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
166
|
+
return typeof raw.version === "string" && raw.version.trim() ? raw.version : "0.0.0";
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return "0.0.0";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
163
172
|
function writeTextFileIfMissing(path, content, label, created) {
|
|
164
173
|
if (existsSync(path)) {
|
|
165
174
|
return false;
|
|
@@ -283,11 +292,16 @@ export function parseArgs(argv, paths = DEFAULT_BOOTSTRAP_PATHS, io = console) {
|
|
|
283
292
|
io.log("Options:");
|
|
284
293
|
io.log(" --sandbox=host Run tools on host (default)");
|
|
285
294
|
io.log(" --sandbox=docker:<name> Run tools in Docker container");
|
|
295
|
+
io.log(" --version Print the current version and exit");
|
|
286
296
|
io.log("");
|
|
287
297
|
io.log(`Config: ${paths.channelConfigPath}`);
|
|
288
298
|
io.log(`Workspace: ${paths.workspaceDir}`);
|
|
289
299
|
throw new BootstrapExitError(0);
|
|
290
300
|
}
|
|
301
|
+
else if (arg === "--version") {
|
|
302
|
+
io.log(readCliVersion());
|
|
303
|
+
throw new BootstrapExitError(0);
|
|
304
|
+
}
|
|
291
305
|
}
|
|
292
306
|
return { sandbox };
|
|
293
307
|
}
|
|
@@ -78,7 +78,13 @@ export declare class DingTalkBot {
|
|
|
78
78
|
private clearKeepAliveTimer;
|
|
79
79
|
private clearReconnectTimer;
|
|
80
80
|
private clearAllTimers;
|
|
81
|
+
private sleep;
|
|
81
82
|
private waitForDelay;
|
|
83
|
+
private waitForSocketState;
|
|
84
|
+
private markClientDisconnected;
|
|
85
|
+
private clearClientSocketReference;
|
|
86
|
+
private cleanupSocket;
|
|
87
|
+
private connectWithTimeout;
|
|
82
88
|
private scheduleReconnect;
|
|
83
89
|
start(): Promise<void>;
|
|
84
90
|
private handleRawMessage;
|
package/dist/runtime/dingtalk.js
CHANGED
|
@@ -54,6 +54,13 @@ class ChannelQueue {
|
|
|
54
54
|
// ============================================================================
|
|
55
55
|
const DINGTALK_API = "https://api.dingtalk.com";
|
|
56
56
|
const TOKEN_REFRESH_SECS = 90 * 60; // 1.5 hours (tokens expire after 2 hours)
|
|
57
|
+
const CONNECT_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
58
|
+
const SOCKET_CLOSE_GRACE_MS = 1_000;
|
|
59
|
+
const SOCKET_TERMINATE_GRACE_MS = 250;
|
|
60
|
+
const SOCKET_STATE_CONNECTING = 0;
|
|
61
|
+
const SOCKET_STATE_OPEN = 1;
|
|
62
|
+
const SOCKET_STATE_CLOSING = 2;
|
|
63
|
+
const SOCKET_STATE_CLOSED = 3;
|
|
57
64
|
// ============================================================================
|
|
58
65
|
// DingTalkBot
|
|
59
66
|
// ============================================================================
|
|
@@ -140,6 +147,12 @@ export class DingTalkBot {
|
|
|
140
147
|
this.clearKeepAliveTimer();
|
|
141
148
|
this.clearReconnectTimer();
|
|
142
149
|
}
|
|
150
|
+
async sleep(delayMs) {
|
|
151
|
+
await new Promise((resolve) => {
|
|
152
|
+
const timer = setTimeout(resolve, delayMs);
|
|
153
|
+
timer.unref?.();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
143
156
|
async waitForDelay(delayMs) {
|
|
144
157
|
await new Promise((resolve) => {
|
|
145
158
|
this.reconnectTimer = this.setTrackedTimeout(() => {
|
|
@@ -148,6 +161,81 @@ export class DingTalkBot {
|
|
|
148
161
|
}, delayMs);
|
|
149
162
|
});
|
|
150
163
|
}
|
|
164
|
+
async waitForSocketState(socket, expectedState, timeoutMs) {
|
|
165
|
+
const deadline = Date.now() + timeoutMs;
|
|
166
|
+
while ((socket.readyState ?? SOCKET_STATE_CLOSED) !== expectedState && Date.now() < deadline) {
|
|
167
|
+
await this.sleep(25);
|
|
168
|
+
}
|
|
169
|
+
return (socket.readyState ?? SOCKET_STATE_CLOSED) === expectedState;
|
|
170
|
+
}
|
|
171
|
+
markClientDisconnected() {
|
|
172
|
+
if (!this.client) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
Reflect.set(this.client, "connected", false);
|
|
176
|
+
Reflect.set(this.client, "registered", false);
|
|
177
|
+
Reflect.set(this.client, "reconnecting", false);
|
|
178
|
+
}
|
|
179
|
+
clearClientSocketReference() {
|
|
180
|
+
if (!this.client) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
Reflect.set(this.client, "socket", undefined);
|
|
184
|
+
}
|
|
185
|
+
async cleanupSocket(reason) {
|
|
186
|
+
const socket = this.getSocket();
|
|
187
|
+
this.markClientDisconnected();
|
|
188
|
+
if (!socket) {
|
|
189
|
+
this.clearClientSocketReference();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
socket.removeAllListeners?.();
|
|
193
|
+
if ((socket.readyState ?? SOCKET_STATE_CLOSED) !== SOCKET_STATE_CLOSED) {
|
|
194
|
+
try {
|
|
195
|
+
socket.close?.();
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
log.logWarning(`DingTalk: socket close failed during ${reason}`, err instanceof Error ? err.message : String(err));
|
|
199
|
+
}
|
|
200
|
+
const closed = await this.waitForSocketState(socket, SOCKET_STATE_CLOSED, SOCKET_CLOSE_GRACE_MS);
|
|
201
|
+
if (!closed) {
|
|
202
|
+
log.logWarning(`DingTalk: forcing socket termination during ${reason}`);
|
|
203
|
+
try {
|
|
204
|
+
socket.terminate?.();
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
log.logWarning(`DingTalk: socket terminate failed during ${reason}`, err instanceof Error ? err.message : String(err));
|
|
208
|
+
}
|
|
209
|
+
await this.waitForSocketState(socket, SOCKET_STATE_CLOSED, SOCKET_TERMINATE_GRACE_MS);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.clearClientSocketReference();
|
|
213
|
+
}
|
|
214
|
+
async connectWithTimeout() {
|
|
215
|
+
if (!this.client) {
|
|
216
|
+
throw new Error("DingTalk client is not initialized");
|
|
217
|
+
}
|
|
218
|
+
const connectPromise = Promise.resolve(this.client.connect());
|
|
219
|
+
let timeoutHandle = null;
|
|
220
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
221
|
+
timeoutHandle = setTimeout(() => {
|
|
222
|
+
reject(new Error(`connect timed out after ${CONNECT_ATTEMPT_TIMEOUT_MS}ms`));
|
|
223
|
+
}, CONNECT_ATTEMPT_TIMEOUT_MS);
|
|
224
|
+
timeoutHandle.unref?.();
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (timeoutHandle) {
|
|
231
|
+
clearTimeout(timeoutHandle);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const socket = this.getSocket();
|
|
235
|
+
if (!socket || socket.readyState !== SOCKET_STATE_OPEN) {
|
|
236
|
+
throw new Error("stream socket did not reach open state");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
151
239
|
scheduleReconnect(delayMs, immediate) {
|
|
152
240
|
if (this.isStopped) {
|
|
153
241
|
return;
|
|
@@ -173,11 +261,13 @@ export class DingTalkBot {
|
|
|
173
261
|
}
|
|
174
262
|
log.logInfo(`DingTalk: initializing stream (clientId=${this.config.clientId.substring(0, 8)}…)`);
|
|
175
263
|
this.clearAllTimers();
|
|
176
|
-
|
|
264
|
+
const clientOptions = {
|
|
177
265
|
clientId: this.config.clientId,
|
|
178
266
|
clientSecret: this.config.clientSecret,
|
|
267
|
+
autoReconnect: false,
|
|
179
268
|
keepAlive: false,
|
|
180
|
-
}
|
|
269
|
+
};
|
|
270
|
+
this.client = new DWClient(clientOptions);
|
|
181
271
|
this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
|
|
182
272
|
return this.handleRawMessage(msg);
|
|
183
273
|
});
|
|
@@ -220,6 +310,8 @@ export class DingTalkBot {
|
|
|
220
310
|
this.isReconnecting = true;
|
|
221
311
|
let connectionFailed = false;
|
|
222
312
|
let connected = false;
|
|
313
|
+
this.clearReconnectTimer();
|
|
314
|
+
this.clearKeepAliveTimer();
|
|
223
315
|
if (!immediate && this.reconnectAttempts > 0) {
|
|
224
316
|
const delay = Math.min(1000 * 2 ** this.reconnectAttempts + Math.random() * 1000, 30000);
|
|
225
317
|
log.logInfo(`DingTalk: waiting ${Math.round(delay / 1000)}s before reconnecting...`);
|
|
@@ -231,10 +323,14 @@ export class DingTalkBot {
|
|
|
231
323
|
}
|
|
232
324
|
try {
|
|
233
325
|
const socket = this.getSocket();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
326
|
+
const readyState = socket?.readyState;
|
|
327
|
+
if (readyState === SOCKET_STATE_CONNECTING ||
|
|
328
|
+
readyState === SOCKET_STATE_OPEN ||
|
|
329
|
+
readyState === SOCKET_STATE_CLOSING ||
|
|
330
|
+
readyState === SOCKET_STATE_CLOSED) {
|
|
331
|
+
await this.cleanupSocket("reconnect");
|
|
332
|
+
}
|
|
333
|
+
await this.connectWithTimeout();
|
|
238
334
|
this.lastSocketAvailableTime = Date.now();
|
|
239
335
|
this.reconnectAttempts = 0; // Success, reset backoff
|
|
240
336
|
log.logInfo("DingTalk: connected to stream.");
|
|
@@ -289,6 +385,7 @@ export class DingTalkBot {
|
|
|
289
385
|
});
|
|
290
386
|
}
|
|
291
387
|
catch (err) {
|
|
388
|
+
await this.cleanupSocket("reconnect failure");
|
|
292
389
|
this.reconnectAttempts++;
|
|
293
390
|
connectionFailed = true;
|
|
294
391
|
log.logWarning("DingTalk: connection failed", err instanceof Error ? err.message : String(err));
|
|
@@ -311,7 +408,7 @@ export class DingTalkBot {
|
|
|
311
408
|
}
|
|
312
409
|
if (this.client) {
|
|
313
410
|
try {
|
|
314
|
-
await
|
|
411
|
+
await this.cleanupSocket("stop");
|
|
315
412
|
}
|
|
316
413
|
catch (err) {
|
|
317
414
|
log.logWarning("DingTalk: failed to disconnect cleanly", err instanceof Error ? err.message : String(err));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oyasmi/pipiclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"clean": "
|
|
25
|
-
"build": "tsc -p tsconfig.build.json &&
|
|
24
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('coverage', { recursive: true, force: true });\"",
|
|
25
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/main.js', 0o755)\"",
|
|
26
26
|
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
27
27
|
"lint": "biome check .",
|
|
28
28
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"@mozilla/readability": "^0.6.0",
|
|
40
40
|
"@sinclair/typebox": "^0.34.0",
|
|
41
41
|
"axios": "^1.7.0",
|
|
42
|
-
"chalk": "^5.6.2",
|
|
43
42
|
"croner": "^9.1.0",
|
|
44
43
|
"diff": "^8.0.2",
|
|
45
44
|
"dingtalk-stream": "^2.1.4",
|
|
@@ -51,11 +50,9 @@
|
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
52
|
"@biomejs/biome": "2.3.5",
|
|
54
|
-
"@types/diff": "^7.0.2",
|
|
55
53
|
"@types/jsdom": "^28.0.1",
|
|
56
54
|
"@types/node": "^24.3.0",
|
|
57
55
|
"@vitest/coverage-v8": "^3.2.4",
|
|
58
|
-
"shx": "^0.4.0",
|
|
59
56
|
"typescript": "^5.7.3",
|
|
60
57
|
"vitest": "^3.2.4"
|
|
61
58
|
},
|