@teammy/server 0.1.13 → 0.1.15

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.
@@ -1,5 +1,5 @@
1
1
  // apps/server/ai/native-pi-entry.ts
2
- import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type2 } from "@earendil-works/pi-ai";
2
+ import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type3 } from "@earendil-works/pi-ai";
3
3
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
4
4
  import {
5
5
  createAgentSession,
@@ -8,7 +8,7 @@ import {
8
8
  SessionManager,
9
9
  SettingsManager
10
10
  } from "@earendil-works/pi-coding-agent";
11
- import { z as z2 } from "zod";
11
+ import { z as z4 } from "zod";
12
12
 
13
13
  // apps/server/ai/agent-channel.ts
14
14
  import { createInterface } from "node:readline";
@@ -60,6 +60,20 @@ import { createModels, createProvider, Type } from "@earendil-works/pi-ai";
60
60
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
61
61
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
62
62
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
63
+
64
+ // apps/server/errors.ts
65
+ var HubError = class extends Error {
66
+ constructor(statusCode, code, message) {
67
+ super(message);
68
+ this.statusCode = statusCode;
69
+ this.code = code;
70
+ this.name = "HubError";
71
+ }
72
+ statusCode;
73
+ code;
74
+ };
75
+
76
+ // apps/server/ai/models.ts
63
77
  function modelAccess(profile, key, reasoning = false) {
64
78
  const model = {
65
79
  id: profile.model,
@@ -109,6 +123,196 @@ function loadAgentSkills(directory = `${agentResourcesPath}/skills`) {
109
123
  return existsSync(directory) ? loadSkillsFromDir({ dir: directory, source: "teammy-deployment" }) : { skills: [], diagnostics: [] };
110
124
  }
111
125
 
126
+ // apps/server/ai/context-content.ts
127
+ import { createHash, randomUUID } from "node:crypto";
128
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
129
+ import { join } from "node:path";
130
+ import { z as z2 } from "zod";
131
+ var CONTENT_BYTES = 32 * 1024;
132
+ var CONTENT_LINES = 500;
133
+ var json = (value) => JSON.stringify(value) ?? "null";
134
+ function contentFits(value, bytes = CONTENT_BYTES) {
135
+ const text = json(value);
136
+ return Buffer.byteLength(text) <= bytes && (text.match(/\\n|\n/g)?.length ?? 0) < CONTENT_LINES;
137
+ }
138
+ var ReadToolResultSchema = z2.object({
139
+ id: z2.string().regex(/^[a-f0-9]{64}$/),
140
+ path: z2.array(z2.union([z2.string().max(512), z2.number().int().nonnegative()])).max(32).refine((path) => Buffer.byteLength(JSON.stringify(path)) <= 4096, "path \u8FC7\u957F").default([]),
141
+ offset: z2.number().int().nonnegative().default(0),
142
+ limit: z2.number().int().positive().max(16e3).optional()
143
+ }).strict();
144
+ var kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
145
+ var length = (value) => typeof value === "string" || Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : 1;
146
+ function textPrefix(text, end) {
147
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1] ?? "")) end--;
148
+ return text.slice(0, end);
149
+ }
150
+ function overview(value) {
151
+ if (contentFits(value, 1024)) return value;
152
+ if (typeof value === "string")
153
+ return { kind: "string", length: value.length, preview: textPrefix(value, 300) };
154
+ if (Array.isArray(value)) return { kind: "array", length: value.length };
155
+ if (value && typeof value === "object")
156
+ return {
157
+ kind: "object",
158
+ fields: Object.entries(value).slice(0, 12).map(([key, item]) => ({
159
+ key: textPrefix(key, 80),
160
+ kind: kind(item),
161
+ length: length(item),
162
+ ...typeof item === "string" ? { preview: textPrefix(item, 120) } : {}
163
+ })),
164
+ totalFields: Object.keys(value).length
165
+ };
166
+ return value;
167
+ }
168
+ var ContextContent = class {
169
+ constructor(directory) {
170
+ this.directory = directory;
171
+ }
172
+ directory;
173
+ async present(value, source) {
174
+ if (contentFits(value)) return value;
175
+ const body = json({ source, value }), id = createHash("sha256").update(body).digest("hex");
176
+ await mkdir(this.directory, { recursive: true, mode: 448 });
177
+ const temporary = join(this.directory, randomUUID() + ".tmp");
178
+ try {
179
+ await writeFile(temporary, body, { flag: "wx", mode: 384 });
180
+ try {
181
+ await link(temporary, join(this.directory, id + ".json"));
182
+ } catch (error) {
183
+ if (error.code !== "EEXIST") throw error;
184
+ }
185
+ } finally {
186
+ await rm(temporary, { force: true });
187
+ }
188
+ return {
189
+ folded: true,
190
+ note: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u6298\u53E0\uFF1B\u4E0B\u65B9\u4EC5\u4E3A\u6982\u89C8\uFF0C\u4E0D\u80FD\u89C6\u4E3A\u5DF2\u8BFB\u5168\u6587\u3002\u7528 read_tool_result \u6309 path \u548C offset \u8BFB\u53D6\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u5FC5\u91CD\u590D\u6267\u884C\u539F\u5DE5\u5177\u3002",
191
+ source,
192
+ id,
193
+ bytes: Buffer.byteLength(json(value)),
194
+ overview: overview(value),
195
+ read: { tool: "read_tool_result", arguments: { id } }
196
+ };
197
+ }
198
+ async input(text) {
199
+ const value = await this.present(text, "\u672C\u8F6E\u8F93\u5165");
200
+ return typeof value === "string" ? value : "\u672C\u8F6E\u8F93\u5165\u8F83\u957F\uFF0C\u539F\u6587\u5DF2\u5B8C\u6574\u4FDD\u5B58\u3002\u5148\u5206\u6BB5\u8BFB\u53D6\uFF0C\u786E\u8BA4\u7528\u6237\u7684\u76EE\u6807\u3001\u6761\u4EF6\u548C\u9650\u5236\u540E\u518D\u884C\u52A8\uFF1B\u6982\u89C8\u4E0D\u662F\u5168\u90E8\u8F93\u5165\u3002\n" + json(value);
201
+ }
202
+ async read(input) {
203
+ const { id, path, offset, limit } = ReadToolResultSchema.parse(input);
204
+ let body;
205
+ try {
206
+ body = await readFile(join(this.directory, id + ".json"), "utf8");
207
+ } catch {
208
+ throw new HubError(
209
+ 404,
210
+ "TOOL_RESULT_NOT_FOUND",
211
+ "\u6B64\u7ED3\u679C\u4E0D\u5728\u5F53\u524D\u4F1A\u8BDD\u6216\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002"
212
+ );
213
+ }
214
+ if (createHash("sha256").update(body).digest("hex") !== id)
215
+ throw new HubError(409, "TOOL_RESULT_CHANGED", "\u7ED3\u679C\u5FEB\u7167\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002");
216
+ const saved = JSON.parse(body);
217
+ let value = saved.value;
218
+ for (const key of path) {
219
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
220
+ throw new HubError(400, "TOOL_RESULT_PATH", "\u7ED3\u679C\u4E2D\u4E0D\u5B58\u5728\u8BE5 path\u3002");
221
+ value = value[key];
222
+ }
223
+ const total = length(value);
224
+ if (offset > total) throw new HubError(400, "TOOL_RESULT_OFFSET", "offset \u8D85\u51FA\u7ED3\u679C\u8303\u56F4\u3002");
225
+ const base = { id, source: saved.source, path, kind: kind(value), total, offset };
226
+ if (typeof value === "string") {
227
+ if (/[\uDC00-\uDFFF]/.test(value[offset] ?? "") && /[\uD800-\uDBFF]/.test(value[offset - 1] ?? ""))
228
+ throw new HubError(400, "TOOL_RESULT_OFFSET", "\u8BF7\u6CBF\u7528\u4E0A\u6B21\u8FD4\u56DE\u7684 nextOffset\u3002");
229
+ const tail = value.slice(offset);
230
+ let lo = 0, hi = Math.min(tail.length, limit ?? 16e3);
231
+ while (lo < hi) {
232
+ const mid = Math.ceil((lo + hi) / 2);
233
+ if (contentFits({ ...base, text: textPrefix(tail, mid), nextOffset: offset + mid }))
234
+ lo = mid;
235
+ else hi = mid - 1;
236
+ }
237
+ let text = textPrefix(tail, lo);
238
+ if (!text && tail) text = textPrefix(tail, 2);
239
+ return {
240
+ ...base,
241
+ text,
242
+ nextOffset: offset + text.length < total ? offset + text.length : null
243
+ };
244
+ }
245
+ if (!value || typeof value !== "object") return { ...base, value, nextOffset: null };
246
+ const entries = [];
247
+ const keys = Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value);
248
+ for (const key of keys.slice(offset, offset + Math.min(limit ?? 50, 100))) {
249
+ const item = value[key];
250
+ const folded = !contentFits(item, 2048);
251
+ const entry = { path: [...path, key], folded, value: folded ? overview(item) : item };
252
+ if (!contentFits({
253
+ ...base,
254
+ entries: [...entries, entry],
255
+ nextOffset: offset + entries.length + 1
256
+ }))
257
+ break;
258
+ entries.push(entry);
259
+ }
260
+ if (!entries.length && offset < total)
261
+ throw new HubError(413, "TOOL_RESULT_KEY_TOO_LONG", "\u7ED3\u679C\u5B57\u6BB5\u540D\u8FC7\u957F\uFF0C\u65E0\u6CD5\u751F\u6210\u5B89\u5168\u7684\u5206\u9875\u3002");
262
+ return {
263
+ ...base,
264
+ entries,
265
+ nextOffset: offset + entries.length < total ? offset + entries.length : null
266
+ };
267
+ }
268
+ };
269
+
270
+ // apps/server/ai/content-tools.ts
271
+ import { Type as Type2 } from "@earendil-works/pi-ai";
272
+ import { z as z3 } from "zod";
273
+ var readToolResultDescription = "\u8BFB\u53D6\u5F53\u524D\u8303\u56F4\u5185\u5DF2\u6298\u53E0\u7684\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u91CD\u8DD1\u539F\u5DE5\u5177\u3002\u5148\u7528 id \u67E5\u770B\u5B57\u6BB5/\u6761\u76EE\uFF1B\u6CBF\u8FD4\u56DE\u7684 path \u8BFB\u53D6\u5177\u4F53\u5B57\u6BB5\u6216\u6587\u4EF6\u5DEE\u5F02\uFF0CnextOffset \u975E\u7A7A\u65F6\u7EE7\u7EED\u3002folded=true \u7684\u6761\u76EE\u53EA\u663E\u793A\u6982\u89C8\uFF0C\u4E0D\u80FD\u5F53\u4F5C\u5B8C\u6574\u6B63\u6587\uFF1B\u5B57\u7B26\u4E32 offset \u4E3A\u8FD4\u56DE\u7684 UTF-16 \u4F4D\u7F6E\u3002\u5FEB\u7167\u56FA\u5B9A\u5728\u539F\u67E5\u8BE2\u7248\u672C\u3002";
274
+ function contentTools(content, tools) {
275
+ return [
276
+ ...tools.map((tool) => ({
277
+ ...tool,
278
+ async execute(...args) {
279
+ const result = await tool.execute(...args);
280
+ const blocks = [];
281
+ for (const block of result.content) {
282
+ if (block.type !== "text") {
283
+ blocks.push(block);
284
+ continue;
285
+ }
286
+ let value;
287
+ try {
288
+ value = JSON.parse(block.text);
289
+ } catch {
290
+ value = block.text;
291
+ }
292
+ const bounded = await content.present(value, tool.name);
293
+ blocks.push({
294
+ type: "text",
295
+ text: bounded === value && contentFits(block.text) ? block.text : JSON.stringify(bounded)
296
+ });
297
+ }
298
+ return { content: blocks, details: {} };
299
+ }
300
+ })),
301
+ {
302
+ name: "read_tool_result",
303
+ label: "\u8BFB\u53D6\u6298\u53E0\u5185\u5BB9",
304
+ description: readToolResultDescription,
305
+ parameters: Type2.Unsafe(z3.toJSONSchema(ReadToolResultSchema, { io: "input" })),
306
+ async execute(_id, input) {
307
+ return {
308
+ content: [{ type: "text", text: JSON.stringify(await content.read(input)) }],
309
+ details: {}
310
+ };
311
+ }
312
+ }
313
+ ];
314
+ }
315
+
112
316
  // apps/server/ai/native-pi-entry.ts
113
317
  var channel = new AgentChannel(process.stdin, process.stdout);
114
318
  var sequence = 0;
@@ -123,6 +327,7 @@ function rpc(request) {
123
327
  async function run(config) {
124
328
  const { model, runtime } = config;
125
329
  const cwd = "/workspaces", agentDir = "/tmp/pi";
330
+ const content = new ContextContent("/tmp/teammy-content");
126
331
  const modelRuntime = await ModelRuntime.create({
127
332
  credentials: new InMemoryCredentialStore(),
128
333
  modelsStore: new InMemoryModelsStore(),
@@ -163,7 +368,7 @@ async function run(config) {
163
368
  const query = {
164
369
  name: "query_tasks",
165
370
  ...runtime.toolText.query_tasks,
166
- parameters: Type2.Unsafe(z2.toJSONSchema(TaskQuerySchema)),
371
+ parameters: Type3.Unsafe(z4.toJSONSchema(TaskQuerySchema)),
167
372
  async execute(_id, params) {
168
373
  const value = await rpc({ method: "query_tasks", input: TaskQuerySchema.parse(params) });
169
374
  return { content: [{ type: "text", text: JSON.stringify(value) }], details: {} };
@@ -182,8 +387,8 @@ async function run(config) {
182
387
  retry: { enabled: false, maxRetries: 0 },
183
388
  cacheWarming: "off"
184
389
  }),
185
- tools: runtime.config.tools,
186
- customTools: runtime.config.tools.includes("query_tasks") ? [query] : []
390
+ tools: [...runtime.config.tools, "read_tool_result"],
391
+ customTools: contentTools(content, runtime.config.tools.includes("query_tasks") ? [query] : [])
187
392
  });
188
393
  session.agent.streamFunction = (_model, context) => {
189
394
  const stream = createAssistantMessageEventStream();
@@ -227,7 +432,7 @@ async function run(config) {
227
432
  });
228
433
  });
229
434
  try {
230
- await session.prompt(config.input);
435
+ await session.prompt(await content.input(config.input));
231
436
  const last = session.messages.filter((message) => message.role === "assistant").at(-1);
232
437
  if (!last || last.stopReason === "error" || last.stopReason === "aborted")
233
438
  throw new Error("Analysis failed.");
@@ -1,9 +1,9 @@
1
1
  // apps/server/agents/pi-entry.ts
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync as existsSync2 } from "node:fs";
4
- import { mkdir, readFile } from "node:fs/promises";
5
- import { join, basename } from "node:path";
6
- import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type2 } from "@earendil-works/pi-ai";
4
+ import { mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
5
+ import { join as join2, basename } from "node:path";
6
+ import { InMemoryCredentialStore, InMemoryModelsStore, Type as Type3 } from "@earendil-works/pi-ai";
7
7
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
8
8
  import {
9
9
  createAgentSession,
@@ -54,6 +54,20 @@ import { createModels, createProvider, Type } from "@earendil-works/pi-ai";
54
54
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
55
55
  import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
56
56
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
57
+
58
+ // apps/server/errors.ts
59
+ var HubError = class extends Error {
60
+ constructor(statusCode, code, message) {
61
+ super(message);
62
+ this.statusCode = statusCode;
63
+ this.code = code;
64
+ this.name = "HubError";
65
+ }
66
+ statusCode;
67
+ code;
68
+ };
69
+
70
+ // apps/server/ai/models.ts
57
71
  function modelAccess(profile, key, reasoning = false) {
58
72
  const model = {
59
73
  id: profile.model,
@@ -103,6 +117,196 @@ function loadAgentSkills(directory = `${agentResourcesPath}/skills`) {
103
117
  return existsSync(directory) ? loadSkillsFromDir({ dir: directory, source: "teammy-deployment" }) : { skills: [], diagnostics: [] };
104
118
  }
105
119
 
120
+ // apps/server/ai/context-content.ts
121
+ import { createHash, randomUUID } from "node:crypto";
122
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
123
+ import { join } from "node:path";
124
+ import { z } from "zod";
125
+ var CONTENT_BYTES = 32 * 1024;
126
+ var CONTENT_LINES = 500;
127
+ var json = (value) => JSON.stringify(value) ?? "null";
128
+ function contentFits(value, bytes = CONTENT_BYTES) {
129
+ const text = json(value);
130
+ return Buffer.byteLength(text) <= bytes && (text.match(/\\n|\n/g)?.length ?? 0) < CONTENT_LINES;
131
+ }
132
+ var ReadToolResultSchema = z.object({
133
+ id: z.string().regex(/^[a-f0-9]{64}$/),
134
+ path: z.array(z.union([z.string().max(512), z.number().int().nonnegative()])).max(32).refine((path) => Buffer.byteLength(JSON.stringify(path)) <= 4096, "path \u8FC7\u957F").default([]),
135
+ offset: z.number().int().nonnegative().default(0),
136
+ limit: z.number().int().positive().max(16e3).optional()
137
+ }).strict();
138
+ var kind = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
139
+ var length = (value) => typeof value === "string" || Array.isArray(value) ? value.length : value && typeof value === "object" ? Object.keys(value).length : 1;
140
+ function textPrefix(text, end) {
141
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1] ?? "")) end--;
142
+ return text.slice(0, end);
143
+ }
144
+ function overview(value) {
145
+ if (contentFits(value, 1024)) return value;
146
+ if (typeof value === "string")
147
+ return { kind: "string", length: value.length, preview: textPrefix(value, 300) };
148
+ if (Array.isArray(value)) return { kind: "array", length: value.length };
149
+ if (value && typeof value === "object")
150
+ return {
151
+ kind: "object",
152
+ fields: Object.entries(value).slice(0, 12).map(([key, item]) => ({
153
+ key: textPrefix(key, 80),
154
+ kind: kind(item),
155
+ length: length(item),
156
+ ...typeof item === "string" ? { preview: textPrefix(item, 120) } : {}
157
+ })),
158
+ totalFields: Object.keys(value).length
159
+ };
160
+ return value;
161
+ }
162
+ var ContextContent = class {
163
+ constructor(directory) {
164
+ this.directory = directory;
165
+ }
166
+ directory;
167
+ async present(value, source) {
168
+ if (contentFits(value)) return value;
169
+ const body = json({ source, value }), id = createHash("sha256").update(body).digest("hex");
170
+ await mkdir(this.directory, { recursive: true, mode: 448 });
171
+ const temporary = join(this.directory, randomUUID() + ".tmp");
172
+ try {
173
+ await writeFile(temporary, body, { flag: "wx", mode: 384 });
174
+ try {
175
+ await link(temporary, join(this.directory, id + ".json"));
176
+ } catch (error) {
177
+ if (error.code !== "EEXIST") throw error;
178
+ }
179
+ } finally {
180
+ await rm(temporary, { force: true });
181
+ }
182
+ return {
183
+ folded: true,
184
+ note: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u6298\u53E0\uFF1B\u4E0B\u65B9\u4EC5\u4E3A\u6982\u89C8\uFF0C\u4E0D\u80FD\u89C6\u4E3A\u5DF2\u8BFB\u5168\u6587\u3002\u7528 read_tool_result \u6309 path \u548C offset \u8BFB\u53D6\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u5FC5\u91CD\u590D\u6267\u884C\u539F\u5DE5\u5177\u3002",
185
+ source,
186
+ id,
187
+ bytes: Buffer.byteLength(json(value)),
188
+ overview: overview(value),
189
+ read: { tool: "read_tool_result", arguments: { id } }
190
+ };
191
+ }
192
+ async input(text) {
193
+ const value = await this.present(text, "\u672C\u8F6E\u8F93\u5165");
194
+ return typeof value === "string" ? value : "\u672C\u8F6E\u8F93\u5165\u8F83\u957F\uFF0C\u539F\u6587\u5DF2\u5B8C\u6574\u4FDD\u5B58\u3002\u5148\u5206\u6BB5\u8BFB\u53D6\uFF0C\u786E\u8BA4\u7528\u6237\u7684\u76EE\u6807\u3001\u6761\u4EF6\u548C\u9650\u5236\u540E\u518D\u884C\u52A8\uFF1B\u6982\u89C8\u4E0D\u662F\u5168\u90E8\u8F93\u5165\u3002\n" + json(value);
195
+ }
196
+ async read(input) {
197
+ const { id, path, offset, limit } = ReadToolResultSchema.parse(input);
198
+ let body;
199
+ try {
200
+ body = await readFile(join(this.directory, id + ".json"), "utf8");
201
+ } catch {
202
+ throw new HubError(
203
+ 404,
204
+ "TOOL_RESULT_NOT_FOUND",
205
+ "\u6B64\u7ED3\u679C\u4E0D\u5728\u5F53\u524D\u4F1A\u8BDD\u6216\u5DF2\u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002"
206
+ );
207
+ }
208
+ if (createHash("sha256").update(body).digest("hex") !== id)
209
+ throw new HubError(409, "TOOL_RESULT_CHANGED", "\u7ED3\u679C\u5FEB\u7167\u5DF2\u6539\u53D8\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u539F\u59CB\u8D44\u6599\u3002");
210
+ const saved = JSON.parse(body);
211
+ let value = saved.value;
212
+ for (const key of path) {
213
+ if (!value || typeof value !== "object" || !Object.hasOwn(value, key))
214
+ throw new HubError(400, "TOOL_RESULT_PATH", "\u7ED3\u679C\u4E2D\u4E0D\u5B58\u5728\u8BE5 path\u3002");
215
+ value = value[key];
216
+ }
217
+ const total = length(value);
218
+ if (offset > total) throw new HubError(400, "TOOL_RESULT_OFFSET", "offset \u8D85\u51FA\u7ED3\u679C\u8303\u56F4\u3002");
219
+ const base = { id, source: saved.source, path, kind: kind(value), total, offset };
220
+ if (typeof value === "string") {
221
+ if (/[\uDC00-\uDFFF]/.test(value[offset] ?? "") && /[\uD800-\uDBFF]/.test(value[offset - 1] ?? ""))
222
+ throw new HubError(400, "TOOL_RESULT_OFFSET", "\u8BF7\u6CBF\u7528\u4E0A\u6B21\u8FD4\u56DE\u7684 nextOffset\u3002");
223
+ const tail = value.slice(offset);
224
+ let lo = 0, hi = Math.min(tail.length, limit ?? 16e3);
225
+ while (lo < hi) {
226
+ const mid = Math.ceil((lo + hi) / 2);
227
+ if (contentFits({ ...base, text: textPrefix(tail, mid), nextOffset: offset + mid }))
228
+ lo = mid;
229
+ else hi = mid - 1;
230
+ }
231
+ let text = textPrefix(tail, lo);
232
+ if (!text && tail) text = textPrefix(tail, 2);
233
+ return {
234
+ ...base,
235
+ text,
236
+ nextOffset: offset + text.length < total ? offset + text.length : null
237
+ };
238
+ }
239
+ if (!value || typeof value !== "object") return { ...base, value, nextOffset: null };
240
+ const entries = [];
241
+ const keys = Array.isArray(value) ? value.map((_, i) => i) : Object.keys(value);
242
+ for (const key of keys.slice(offset, offset + Math.min(limit ?? 50, 100))) {
243
+ const item = value[key];
244
+ const folded = !contentFits(item, 2048);
245
+ const entry = { path: [...path, key], folded, value: folded ? overview(item) : item };
246
+ if (!contentFits({
247
+ ...base,
248
+ entries: [...entries, entry],
249
+ nextOffset: offset + entries.length + 1
250
+ }))
251
+ break;
252
+ entries.push(entry);
253
+ }
254
+ if (!entries.length && offset < total)
255
+ throw new HubError(413, "TOOL_RESULT_KEY_TOO_LONG", "\u7ED3\u679C\u5B57\u6BB5\u540D\u8FC7\u957F\uFF0C\u65E0\u6CD5\u751F\u6210\u5B89\u5168\u7684\u5206\u9875\u3002");
256
+ return {
257
+ ...base,
258
+ entries,
259
+ nextOffset: offset + entries.length < total ? offset + entries.length : null
260
+ };
261
+ }
262
+ };
263
+
264
+ // apps/server/ai/content-tools.ts
265
+ import { Type as Type2 } from "@earendil-works/pi-ai";
266
+ import { z as z2 } from "zod";
267
+ var readToolResultDescription = "\u8BFB\u53D6\u5F53\u524D\u8303\u56F4\u5185\u5DF2\u6298\u53E0\u7684\u539F\u59CB\u7ED3\u679C\uFF0C\u4E0D\u91CD\u8DD1\u539F\u5DE5\u5177\u3002\u5148\u7528 id \u67E5\u770B\u5B57\u6BB5/\u6761\u76EE\uFF1B\u6CBF\u8FD4\u56DE\u7684 path \u8BFB\u53D6\u5177\u4F53\u5B57\u6BB5\u6216\u6587\u4EF6\u5DEE\u5F02\uFF0CnextOffset \u975E\u7A7A\u65F6\u7EE7\u7EED\u3002folded=true \u7684\u6761\u76EE\u53EA\u663E\u793A\u6982\u89C8\uFF0C\u4E0D\u80FD\u5F53\u4F5C\u5B8C\u6574\u6B63\u6587\uFF1B\u5B57\u7B26\u4E32 offset \u4E3A\u8FD4\u56DE\u7684 UTF-16 \u4F4D\u7F6E\u3002\u5FEB\u7167\u56FA\u5B9A\u5728\u539F\u67E5\u8BE2\u7248\u672C\u3002";
268
+ function contentTools(content, tools) {
269
+ return [
270
+ ...tools.map((tool) => ({
271
+ ...tool,
272
+ async execute(...args) {
273
+ const result = await tool.execute(...args);
274
+ const blocks = [];
275
+ for (const block of result.content) {
276
+ if (block.type !== "text") {
277
+ blocks.push(block);
278
+ continue;
279
+ }
280
+ let value;
281
+ try {
282
+ value = JSON.parse(block.text);
283
+ } catch {
284
+ value = block.text;
285
+ }
286
+ const bounded = await content.present(value, tool.name);
287
+ blocks.push({
288
+ type: "text",
289
+ text: bounded === value && contentFits(block.text) ? block.text : JSON.stringify(bounded)
290
+ });
291
+ }
292
+ return { content: blocks, details: {} };
293
+ }
294
+ })),
295
+ {
296
+ name: "read_tool_result",
297
+ label: "\u8BFB\u53D6\u6298\u53E0\u5185\u5BB9",
298
+ description: readToolResultDescription,
299
+ parameters: Type2.Unsafe(z2.toJSONSchema(ReadToolResultSchema, { io: "input" })),
300
+ async execute(_id, input) {
301
+ return {
302
+ content: [{ type: "text", text: JSON.stringify(await content.read(input)) }],
303
+ details: {}
304
+ };
305
+ }
306
+ }
307
+ ];
308
+ }
309
+
106
310
  // apps/server/agents/pi-entry.ts
107
311
  async function piChild() {
108
312
  const channel = new AgentChannel(process.stdin, process.stdout);
@@ -128,7 +332,7 @@ async function piChild() {
128
332
  ]);
129
333
  if (session && key === configKey) return;
130
334
  session?.dispose();
131
- await mkdir(input.sessionDir, { recursive: true });
335
+ await mkdir2(input.sessionDir, { recursive: true });
132
336
  manager = input.transient ? SessionManager.inMemory(input.cwd ?? "/project") : input.sessionFile && existsSync2(input.sessionFile) ? SessionManager.open(input.sessionFile, input.sessionDir, input.cwd ?? "/project") : SessionManager.create(input.cwd ?? "/project", input.sessionDir);
133
337
  const modelRuntime = await ModelRuntime.create({
134
338
  credentials: new InMemoryCredentialStore(),
@@ -170,23 +374,24 @@ async function piChild() {
170
374
  reload: async () => {
171
375
  }
172
376
  };
377
+ const content = new ContextContent(join2(input.sessionDir, "content"));
173
378
  const customTools = input.tools.map((tool) => ({
174
379
  name: tool.name,
175
380
  label: tool.label,
176
381
  description: tool.description,
177
- parameters: Type2.Unsafe(tool.schema),
382
+ parameters: Type3.Unsafe(tool.schema),
178
383
  execute: async (_id, params) => {
179
384
  let input2 = params;
180
385
  if (tool.name === "save_material") {
181
386
  const source = params;
182
387
  input2 = {
183
388
  ...source,
184
- ...source.contentFile ? { _content: await readFile(source.contentFile, "utf8") } : {},
389
+ ...source.contentFile ? { _content: await readFile2(source.contentFile, "utf8") } : {},
185
390
  ...source.originals ? {
186
391
  _files: await Promise.all(
187
392
  source.originals.map(async (file) => ({
188
393
  name: file.name ?? basename(file.path),
189
- base64: (await readFile(file.path)).toString("base64")
394
+ base64: (await readFile2(file.path)).toString("base64")
190
395
  }))
191
396
  )
192
397
  } : {}
@@ -198,7 +403,7 @@ async function piChild() {
198
403
  }));
199
404
  const created = await createAgentSession({
200
405
  cwd: input.cwd ?? "/project",
201
- agentDir: join(input.sessionDir, "agent"),
406
+ agentDir: join2(input.sessionDir, "agent"),
202
407
  modelRuntime,
203
408
  model: input.model,
204
409
  thinkingLevel: "off",
@@ -216,9 +421,10 @@ async function piChild() {
216
421
  "find",
217
422
  "ls",
218
423
  ...input.cwd === "/work" ? ["write", "edit"] : [],
219
- ...input.tools.map((tool) => tool.name)
424
+ ...input.tools.map((tool) => tool.name),
425
+ "read_tool_result"
220
426
  ],
221
- customTools
427
+ customTools: contentTools(content, customTools)
222
428
  });
223
429
  session = created.session;
224
430
  configKey = key;
@@ -298,8 +504,10 @@ async function piChild() {
298
504
  });
299
505
  try {
300
506
  await native.prompt(
301
- interrupted ? `\u6B64\u524D\u8FD9\u4E00\u8F6E\u6267\u884C\u4E2D\u65AD\u6216\u672A\u6210\u529F\uFF0C\u8BF7\u7ED3\u5408\u5DF2\u4FDD\u5B58\u7684\u4E0A\u4E0B\u6587\u7EE7\u7EED\u3002\u5DF2\u63D0\u4EA4\u64CD\u4F5C\u7684\u56DE\u6267\u4ECD\u6709\u6548\uFF0C\u4E0D\u8981\u91CD\u590D\u53D1\u9001\uFF1B\u6682\u5B58\u56DE\u7B54\u548C\u5B8C\u6210\u62A5\u544A\u5C1A\u672A\u63D0\u4EA4\uFF0C\u8BF7\u91CD\u65B0\u6838\u5BF9\u540E\u7ED9\u51FA\u672C\u8F6E\u7ED3\u8BBA\u3002\u539F\u8F93\u5165\uFF1A
507
+ await new ContextContent(join2(input.sessionDir, "content")).input(
508
+ interrupted ? `\u6B64\u524D\u8FD9\u4E00\u8F6E\u6267\u884C\u4E2D\u65AD\u6216\u672A\u6210\u529F\uFF0C\u8BF7\u7ED3\u5408\u5DF2\u4FDD\u5B58\u7684\u4E0A\u4E0B\u6587\u7EE7\u7EED\u3002\u5DF2\u63D0\u4EA4\u64CD\u4F5C\u7684\u56DE\u6267\u4ECD\u6709\u6548\uFF0C\u4E0D\u8981\u91CD\u590D\u53D1\u9001\uFF1B\u6682\u5B58\u56DE\u7B54\u548C\u5B8C\u6210\u62A5\u544A\u5C1A\u672A\u63D0\u4EA4\uFF0C\u8BF7\u91CD\u65B0\u6838\u5BF9\u540E\u7ED9\u51FA\u672C\u8F6E\u7ED3\u8BBA\u3002\u539F\u8F93\u5165\uFF1A
302
509
  ${input.text}` : input.text
510
+ )
303
511
  );
304
512
  await native.waitForIdle();
305
513
  const last = native.messages.filter((message) => message.role === "assistant").at(-1);
@@ -1 +1 @@
1
- import{$ as e,A as t,D as n,J as r,M as i,O as a,S as o,W as s,a as c,b as l,et as ee,f as u,i as d,j as te,k as f,n as p,o as m,q as h,r as ne,t as re,u as ie,v as ae,y as oe}from"./index-BTcnf4jy.js";import{t as se}from"./Markdown-VmpJdXUC.js";var g=e(),_=t(),v=e=>new Date(e).toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`}),y=e=>e.replace(/[#*_`>]/g,``).replace(/\s+/g,` `).trim();function b({mine:e=!1,author:t,time:n,children:r}){return(0,_.jsx)(`div`,{className:`assistant-bubble-row ${e?`mine`:``}`,children:(0,_.jsxs)(`div`,{className:`assistant-bubble`,children:[(0,_.jsxs)(`div`,{className:`assistant-bubble-meta`,children:[(0,_.jsx)(`span`,{children:t}),(0,_.jsx)(`time`,{dateTime:n,children:v(n)})]}),r]})})}function x({me:e}){let{projectId:t,request:x}=ie(),C=f(),w=a(t,e,`assistant-selection`),T=a(t,e,`assistant-drafts`),ce=(0,g.useCallback)(e=>x(`agents/outbox`,c,{signal:e}),[x]),E=n(ce,3e3,a(t,e,`outbox`)),le=(0,g.useCallback)(e=>x(`agents/chats`,re,{signal:e}),[x]),D=n(le,3e3,a(t,e,`chats`)),[O,ue]=(0,g.useState)(()=>{let e=new URLSearchParams(location.search),t=e.get(`chat`);return t?`chat:${t}`:e.get(`message`)??C?.get(w)??``}),k=O.startsWith(`chat:`)?O.slice(5):``,A=!O||!!k,[j,M]=(0,g.useState)(()=>C?.get(a(t,e,`chat`,k))),N=j?.id===k?j:C?.get(a(t,e,`chat`,k)),de=D.data?.chats.find(e=>e.id===k),[fe,P]=(0,g.useState)(),[F,I]=(0,g.useState)(()=>C?.get(T)??{}),L=F[O]??``,R=e=>I(t=>({...t,[O]:e})),[z,B]=(0,g.useState)(!1),[V,H]=(0,g.useState)(!1),[pe,U]=(0,g.useState)(``),[W,G]=(0,g.useState)(``),K=(0,g.useRef)(new Map),me=(0,g.useRef)(null),q=(0,g.useRef)(!0),J=(0,g.useRef)(O);J.current=O,(0,g.useEffect)(()=>{j&&C?.set(a(t,e,`chat`,j.id),j)},[C,t,e,j]),(0,g.useEffect)(()=>{C?.set(T,F)},[C,T,F]);function Y(e){J.current=e,ue(e),C?.set(w,e);let t=new URL(location.href);e.startsWith(`chat:`)?t.searchParams.set(`chat`,e.slice(5)):t.searchParams.delete(`chat`),e&&!e.startsWith(`chat:`)&&e!==`debug`?t.searchParams.set(`message`,e):t.searchParams.delete(`message`),history.replaceState(history.state,``,t)}(0,g.useEffect)(()=>{!O&&D.data?.chats.length&&Y(`chat:${D.data.chats[0].id}`)},[O,D.data]);let X=(E.data?.messages??[]).filter(e=>e.box===`debug`).reverse(),Z=E.data?.messages.find(e=>e.id===(O===`debug`?fe:O));(0,g.useEffect)(()=>{O!==`debug`&&Z?.conversationId&&Y(`chat:${Z.conversationId}`)},[O,Z?.conversationId]);let Q=X.filter(e=>!e.readAt).map(e=>e.id).join(`,`);(0,g.useEffect)(()=>{Z&&!Z.readAt&&O!==`debug`&&x(`agents/outbox/${Z.id}/read`,m,{method:`POST`}).then(()=>{window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}).catch(e=>U(i(e)))},[Z?.id,Z?.readAt,O,x,E.reload]),(0,g.useEffect)(()=>{O===`debug`&&Q&&Promise.all(Q.split(`,`).map(e=>x(`agents/outbox/${e}/read`,m,{method:`POST`}))).then(()=>{window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}).catch(e=>U(i(e)))},[O,Q,x,E.reload]),(0,g.useEffect)(()=>{if(G(``),!k)return;let n=!0,o=-1,s=!1,c=new AbortController,l=e=>{!n||e.id!==k||e.cursor<o||(o=e.cursor,s=!0,e.redirect?Y(`chat:${e.redirect}`):(M(e),G(``)))};x(`agents/chats/${k}`,p,{signal:c.signal}).then(l).catch(r=>{n&&!c.signal.aborted&&(r instanceof te&&[401,403,404].includes(r.status)&&(C?.delete(a(t,e,`chat`,k)),M(e=>e?.id===k?void 0:e)),s||G(i(r)))});let u=e=>{let t=Number(e.lastEventId);return!n||!Number.isSafeInteger(t)||t<=o?!1:(o=t,!0)},f=new EventSource(ee(`/api/v1/projects/${encodeURIComponent(t)}/agents/chats/${encodeURIComponent(k)}/events`));f.addEventListener(`snapshot`,e=>{if(n)try{l(p.parse(JSON.parse(e.data)))}catch(e){G(i(e))}}),f.onerror=()=>{n&&G(`对话连接中断,正在重新连接…`)},f.addEventListener(`context`,e=>{if(!u(e))return;let t=p.pick({kind:!0,title:!0,status:!0,visibility:!0,participants:!0,pending:!0,hold:!0,redirect:!0}).parse(JSON.parse(e.data));t.redirect?Y(`chat:${t.redirect}`):M(e=>e?.id===k?{...e,...t,cursor:o}:e)}),f.addEventListener(`message`,e=>{if(!u(e))return;let t=ne.parse(JSON.parse(e.data));M(n=>n?.id===k?{...n,readCursor:Math.max(n.readCursor,t.seq),cursor:Math.max(n.cursor,Number(e.lastEventId)||0),messages:[...n.messages.filter(e=>e.id!==t.id),t].sort((e,t)=>e.seq-t.seq)}:n)}),f.addEventListener(`message_removed`,e=>{if(!u(e))return;let{id:t}=h({id:r()}).parse(JSON.parse(e.data));M(e=>e?.id===k?{...e,cursor:o,messages:e.messages.filter(e=>e.id!==t)}:e)}),f.addEventListener(`run`,e=>{if(!u(e))return;let t=d.parse(JSON.parse(e.data));M(n=>n?.id===k?{...n,cursor:Math.max(n.cursor,Number(e.lastEventId)||0),runs:n.runs.some(e=>e.id===t.id)?n.runs:[...n.runs,t]}:n)});for(let e of[`delta`,`reset`,`state`])f.addEventListener(e,t=>{if(!u(t))return;let n=JSON.parse(t.data);M(r=>r?.id===k?{...r,cursor:Math.max(r.cursor,Number(t.lastEventId)||0),messages:r.messages.map(t=>t.runId!==n.id||t.role!==`assistant`?t:{...t,body:e===`delta`?t.body+(n.text??``):e===`reset`?``:n.answer??t.body,state:n.state??t.state}),runs:r.runs.map(t=>t.id===n.id?{...t,answer:e===`delta`?t.answer+(n.text??``):e===`reset`?``:n.answer??t.answer,state:n.state??t.state,error:n.error===void 0?t.error:n.error}:t)}:r)});return()=>{n=!1,c.abort(),f.close()}},[t,k,e,x,C]),(0,g.useEffect)(()=>{if(!N?.kind||N.id!==k)return;let e=()=>{document.visibilityState===`visible`&&x(`agents/chats/${k}/read`,h({ok:s()}),{method:`POST`,body:{cursor:N.readCursor}}).then(()=>{D.reload(),window.dispatchEvent(new Event(`teammy:outbox`))}).catch(e=>U(i(e)))},t=setTimeout(e,300);return document.addEventListener(`visibilitychange`,e),()=>{clearTimeout(t),document.removeEventListener(`visibilitychange`,e)}},[k,N?.readCursor,N?.kind,x]),(0,g.useEffect)(()=>{q.current&&me.current?.scrollIntoView({block:`end`})},[O,j?.messages,Z?.recipients,X.length]);function he(){window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}async function $(e){if(q.current=!0,Y(e),U(``),P(void 0),e.startsWith(`chat:`)||e===`debug`)return;let t=E.data?.messages.find(t=>t.id===e)?.conversationId;if(t)Y(`chat:${t}`);else try{await x(`agents/outbox/${e}/read`,m,{method:`POST`}),he()}catch(e){U(i(e))}}async function ge(){if(!V){H(!0),U(``);try{let e=await x(`agents/chats`,p,{method:`POST`});M(e),await $(`chat:${e.id}`),D.reload()}catch(e){U(i(e))}finally{H(!1)}}}async function _e(e){e.preventDefault();let t=L.trim();if(!t||z||V)return;B(!0),U(``);let n=K.current.get(O);n&&n.text===t||(n={text:t,id:crypto.randomUUID()},K.current.set(O,n));try{if(A){let e=n.target??k;if(!e){let r=await x(`agents/chats`,p,{method:`POST`});e=r.id,n.target=e,K.current.set(`chat:${e}`,n),I(n=>({...n,[`chat:${e}`]:t})),J.current===O&&(M(r),Y(`chat:${e}`))}let i=await x(`agents/chats/${e}`,d.extend({conversationId:r().optional()}),{method:`POST`,body:{text:t,requestId:n.id}});i.conversationId&&i.conversationId!==e&&(J.current===O||J.current===`chat:${e}`)&&Y(`chat:${i.conversationId}`),M(t=>t?.id===e?{...t,runs:t.runs.some(e=>e.id===i.id)?t.runs:[...t.runs,i]}:t),D.reload()}else{let e=await x(`agents/outbox/${Z.id}/reply`,h({conversationId:r()}),{method:`POST`,body:{body:t,requestId:n.id}});he(),J.current===O&&Y(`chat:${e.conversationId}`),D.reload(),P(void 0)}K.current.delete(O),n.target&&K.current.delete(`chat:${n.target}`),I(e=>({...e,[O]:``,...n.target?{[`chat:${n.target}`]:``}:{}}))}catch(e){U(i(e))}finally{B(!1)}}let ve=A||!!Z&&!Z.archivedAt,ye=A&&!N&&(!!k||!D.data);return(0,_.jsxs)(`div`,{className:`split-layout assistant-layout`,"data-testid":`assistant-page`,children:[(0,_.jsxs)(`aside`,{className:`assistant-list`,"aria-label":`消息列表`,children:[(0,_.jsxs)(`header`,{className:`assistant-list-heading`,children:[(0,_.jsx)(`h1`,{children:`消息`}),(0,_.jsx)(oe,{className:`assistant-new-chat`,icon:`plus`,label:`新建对话`,disabled:V,onClick:()=>void ge(),"data-testid":`assistant-launcher`})]}),(0,_.jsxs)(`div`,{className:`assistant-message-scroll`,children:[D.loading&&!D.data&&(0,_.jsx)(`p`,{className:`assistant-list-status`,role:`status`,children:`加载中…`}),D.data?.chats.map(e=>(0,_.jsxs)(`button`,{type:`button`,"data-testid":`assistant-chat-${e.id}`,className:`assistant-message-item assistant-chat-item ${k===e.id?`selected`:``}`,"aria-pressed":k===e.id,onClick:()=>void $(`chat:${e.id}`),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${e.unread||e.state===`running`||e.state===`queued`?`unread`:``}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:k===e.id,children:(0,_.jsx)(`strong`,{children:e.title})}),(0,_.jsxs)(`time`,{dateTime:e.updatedAt,children:[new Date(e.updatedAt).toLocaleDateString([],{month:`numeric`,day:`numeric`}),` `,v(e.updatedAt)]})]})]},e.id)),!!X.length&&(0,_.jsxs)(`button`,{type:`button`,"data-testid":`debug-outbox`,className:`assistant-message-item ${O===`debug`?`selected`:``}`,onClick:()=>void $(`debug`),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${Q?`unread`:``}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:O===`debug`,children:(0,_.jsx)(`strong`,{children:`运行提醒`})}),(0,_.jsx)(`span`,{children:y(X.at(-1).body)})]})]}),E.data?.messages.filter(e=>e.box!==`debug`&&!e.conversationId).sort((e,t)=>Number(!!e.archivedAt)-Number(!!t.archivedAt)).map(e=>(0,_.jsxs)(`button`,{type:`button`,"data-testid":`outbox-${e.id}`,className:`assistant-message-item ${e.archivedAt?`archived`:``} ${O===e.id?`selected`:``}`,onClick:()=>void $(e.id),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${e.readAt?``:`unread`}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:O===e.id,children:(0,_.jsx)(`strong`,{children:y(e.body)})}),(0,_.jsxs)(`time`,{dateTime:e.createdAt,children:[new Date(e.createdAt).toLocaleDateString([],{month:`numeric`,day:`numeric`}),` `,v(e.createdAt)]})]})]},e.id))]}),(0,_.jsx)(o,{error:!0,children:E.error||D.error})]}),(0,_.jsxs)(`section`,{className:`assistant-thread`,"aria-label":A?`Tim对话`:`消息内容`,children:[(0,_.jsxs)(`header`,{className:`assistant-thread-heading`,children:[(0,_.jsx)(`h2`,{children:A?de?.title??N?.title??(k?`无标题`:`Tim`):O===`debug`?`运行提醒`:`项目消息`}),N?.kind===`matter`&&(0,_.jsxs)(`span`,{className:`assistant-participants`,title:N.participants?.join(`、`),children:[N.visibility===`project`?`项目共享 · `:``,N.participants?.join(`、`)]})]}),N?.kind===`matter`&&(N.hold||N.pending||N.status===`completed`)&&(0,_.jsx)(`div`,{className:`assistant-matter-state`,role:`status`,children:N.hold?`${N.hold.scope===`all`?`已暂停`:`暂缓修改`} · ${N.hold.reason}`:N.pending?`待确认 · ${N.pending.members.join(`、`)}:${N.pending.question}`:`已完成`}),(0,_.jsxs)(`div`,{className:`assistant-transcript`,role:`log`,"aria-live":`polite`,"data-testid":`assistant-transcript`,onScroll:e=>{let t=e.currentTarget;q.current=t.scrollHeight-t.scrollTop-t.clientHeight<100},children:[A?ye?(0,_.jsxs)(`div`,{className:`assistant-empty`,"data-testid":`assistant-loading`,children:[(0,_.jsx)(l,{}),(0,_.jsx)(`p`,{role:`status`,children:W||D.error?`暂时无法加载对话`:`加载中…`})]}):N?.messages.length?N.messages.filter(e=>e.body||[`queued`,`running`,`failed`].includes(e.state)).map(t=>(0,_.jsx)(`div`,{className:`assistant-turn`,children:(0,_.jsxs)(b,{mine:t.role===`user`&&t.author===e,author:`${t.role===`assistant`?`Tim`:t.author}${t.source===`wecom`?` · 企业微信`:``}`,time:t.createdAt,children:[t.body&&(0,_.jsx)(`div`,{className:`markdown`,children:(0,_.jsx)(se,{children:t.body})}),t.role===`assistant`&&[`queued`,`running`].includes(t.state)&&(0,_.jsxs)(`span`,{className:`assistant-typing`,"aria-label":`正在回答`,children:[(0,_.jsx)(`i`,{}),(0,_.jsx)(`i`,{}),(0,_.jsx)(`i`,{})]}),t.state===`failed`&&(0,_.jsx)(o,{error:!0,children:N.runs.find(e=>e.id===t.runId)?.error||`本次回答未完成。`})]})},t.id)):(0,_.jsxs)(`div`,{className:`assistant-empty`,children:[(0,_.jsx)(l,{}),(0,_.jsx)(`p`,{children:`有什么项目问题想聊聊?`})]}):!E.data&&E.loading?(0,_.jsx)(`div`,{className:`assistant-empty`,children:(0,_.jsx)(`p`,{role:`status`,children:`加载中…`})}):O===`debug`?X.map(e=>(0,_.jsx)(S,{item:e,onReply:e.archivedAt?void 0:()=>{P(e.id),R(``),U(``)}},e.id)):Z&&(0,_.jsx)(S,{item:Z}),(0,_.jsx)(`div`,{ref:me})]}),(0,_.jsx)(o,{error:!0,children:pe||(A?W:``)}),ve?(0,_.jsxs)(`form`,{className:`assistant-composer`,onSubmit:e=>void _e(e),children:[(0,_.jsx)(`textarea`,{value:L,disabled:V,onChange:e=>R(e.target.value),"aria-label":A?`向Tim提问`:`回复消息`,placeholder:A?`问问Tim…`:`回复…`,rows:2,onKeyDown:e=>{e.key===`Enter`&&!e.shiftKey&&!e.nativeEvent.isComposing&&(e.preventDefault(),e.currentTarget.form?.requestSubmit())}}),(0,_.jsx)(`button`,{className:`primary assistant-send`,type:`submit`,disabled:z||V||ye||!L.trim(),"aria-label":`发送`,children:(0,_.jsx)(ae,{name:`send`})})]}):O!==`debug`&&Z&&(0,_.jsxs)(`div`,{className:`assistant-archived`,children:[(0,_.jsx)(ae,{name:`archive`}),`已归档`]})]})]})}function S({item:e,onReply:t}){return(0,_.jsx)(`div`,{"data-testid":`outbox-thread-${e.id}`,children:(0,_.jsxs)(b,{author:e.box===`debug`?`运行提醒`:`Tim`,time:e.createdAt,children:[(0,_.jsx)(`div`,{className:`markdown`,children:(0,_.jsx)(se,{children:e.body})}),t&&(0,_.jsx)(`button`,{type:`button`,className:`assistant-debug-reply`,onClick:t,children:`回复`})]})})}export{x as Assistant};
1
+ import{$ as e,A as t,D as n,J as r,M as i,O as a,S as o,W as s,a as c,b as l,et as ee,f as u,i as d,j as te,k as f,n as p,o as m,q as h,r as ne,t as re,u as ie,v as ae,y as oe}from"./index-DSQiL6vM.js";import{t as se}from"./Markdown-ylYdxJU0.js";var g=e(),_=t(),v=e=>new Date(e).toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`}),y=e=>e.replace(/[#*_`>]/g,``).replace(/\s+/g,` `).trim();function b({mine:e=!1,author:t,time:n,children:r}){return(0,_.jsx)(`div`,{className:`assistant-bubble-row ${e?`mine`:``}`,children:(0,_.jsxs)(`div`,{className:`assistant-bubble`,children:[(0,_.jsxs)(`div`,{className:`assistant-bubble-meta`,children:[(0,_.jsx)(`span`,{children:t}),(0,_.jsx)(`time`,{dateTime:n,children:v(n)})]}),r]})})}function x({me:e}){let{projectId:t,request:x}=ie(),C=f(),w=a(t,e,`assistant-selection`),T=a(t,e,`assistant-drafts`),ce=(0,g.useCallback)(e=>x(`agents/outbox`,c,{signal:e}),[x]),E=n(ce,3e3,a(t,e,`outbox`)),le=(0,g.useCallback)(e=>x(`agents/chats`,re,{signal:e}),[x]),D=n(le,3e3,a(t,e,`chats`)),[O,ue]=(0,g.useState)(()=>{let e=new URLSearchParams(location.search),t=e.get(`chat`);return t?`chat:${t}`:e.get(`message`)??C?.get(w)??``}),k=O.startsWith(`chat:`)?O.slice(5):``,A=!O||!!k,[j,M]=(0,g.useState)(()=>C?.get(a(t,e,`chat`,k))),N=j?.id===k?j:C?.get(a(t,e,`chat`,k)),de=D.data?.chats.find(e=>e.id===k),[fe,P]=(0,g.useState)(),[F,I]=(0,g.useState)(()=>C?.get(T)??{}),L=F[O]??``,R=e=>I(t=>({...t,[O]:e})),[z,B]=(0,g.useState)(!1),[V,H]=(0,g.useState)(!1),[pe,U]=(0,g.useState)(``),[W,G]=(0,g.useState)(``),K=(0,g.useRef)(new Map),me=(0,g.useRef)(null),q=(0,g.useRef)(!0),J=(0,g.useRef)(O);J.current=O,(0,g.useEffect)(()=>{j&&C?.set(a(t,e,`chat`,j.id),j)},[C,t,e,j]),(0,g.useEffect)(()=>{C?.set(T,F)},[C,T,F]);function Y(e){J.current=e,ue(e),C?.set(w,e);let t=new URL(location.href);e.startsWith(`chat:`)?t.searchParams.set(`chat`,e.slice(5)):t.searchParams.delete(`chat`),e&&!e.startsWith(`chat:`)&&e!==`debug`?t.searchParams.set(`message`,e):t.searchParams.delete(`message`),history.replaceState(history.state,``,t)}(0,g.useEffect)(()=>{!O&&D.data?.chats.length&&Y(`chat:${D.data.chats[0].id}`)},[O,D.data]);let X=(E.data?.messages??[]).filter(e=>e.box===`debug`).reverse(),Z=E.data?.messages.find(e=>e.id===(O===`debug`?fe:O));(0,g.useEffect)(()=>{O!==`debug`&&Z?.conversationId&&Y(`chat:${Z.conversationId}`)},[O,Z?.conversationId]);let Q=X.filter(e=>!e.readAt).map(e=>e.id).join(`,`);(0,g.useEffect)(()=>{Z&&!Z.readAt&&O!==`debug`&&x(`agents/outbox/${Z.id}/read`,m,{method:`POST`}).then(()=>{window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}).catch(e=>U(i(e)))},[Z?.id,Z?.readAt,O,x,E.reload]),(0,g.useEffect)(()=>{O===`debug`&&Q&&Promise.all(Q.split(`,`).map(e=>x(`agents/outbox/${e}/read`,m,{method:`POST`}))).then(()=>{window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}).catch(e=>U(i(e)))},[O,Q,x,E.reload]),(0,g.useEffect)(()=>{if(G(``),!k)return;let n=!0,o=-1,s=!1,c=new AbortController,l=e=>{!n||e.id!==k||e.cursor<o||(o=e.cursor,s=!0,e.redirect?Y(`chat:${e.redirect}`):(M(e),G(``)))};x(`agents/chats/${k}`,p,{signal:c.signal}).then(l).catch(r=>{n&&!c.signal.aborted&&(r instanceof te&&[401,403,404].includes(r.status)&&(C?.delete(a(t,e,`chat`,k)),M(e=>e?.id===k?void 0:e)),s||G(i(r)))});let u=e=>{let t=Number(e.lastEventId);return!n||!Number.isSafeInteger(t)||t<=o?!1:(o=t,!0)},f=new EventSource(ee(`/api/v1/projects/${encodeURIComponent(t)}/agents/chats/${encodeURIComponent(k)}/events`));f.addEventListener(`snapshot`,e=>{if(n)try{l(p.parse(JSON.parse(e.data)))}catch(e){G(i(e))}}),f.onerror=()=>{n&&G(`对话连接中断,正在重新连接…`)},f.addEventListener(`context`,e=>{if(!u(e))return;let t=p.pick({kind:!0,title:!0,status:!0,visibility:!0,participants:!0,pending:!0,hold:!0,redirect:!0}).parse(JSON.parse(e.data));t.redirect?Y(`chat:${t.redirect}`):M(e=>e?.id===k?{...e,...t,cursor:o}:e)}),f.addEventListener(`message`,e=>{if(!u(e))return;let t=ne.parse(JSON.parse(e.data));M(n=>n?.id===k?{...n,readCursor:Math.max(n.readCursor,t.seq),cursor:Math.max(n.cursor,Number(e.lastEventId)||0),messages:[...n.messages.filter(e=>e.id!==t.id),t].sort((e,t)=>e.seq-t.seq)}:n)}),f.addEventListener(`message_removed`,e=>{if(!u(e))return;let{id:t}=h({id:r()}).parse(JSON.parse(e.data));M(e=>e?.id===k?{...e,cursor:o,messages:e.messages.filter(e=>e.id!==t)}:e)}),f.addEventListener(`run`,e=>{if(!u(e))return;let t=d.parse(JSON.parse(e.data));M(n=>n?.id===k?{...n,cursor:Math.max(n.cursor,Number(e.lastEventId)||0),runs:n.runs.some(e=>e.id===t.id)?n.runs:[...n.runs,t]}:n)});for(let e of[`delta`,`reset`,`state`])f.addEventListener(e,t=>{if(!u(t))return;let n=JSON.parse(t.data);M(r=>r?.id===k?{...r,cursor:Math.max(r.cursor,Number(t.lastEventId)||0),messages:r.messages.map(t=>t.runId!==n.id||t.role!==`assistant`?t:{...t,body:e===`delta`?t.body+(n.text??``):e===`reset`?``:n.answer??t.body,state:n.state??t.state}),runs:r.runs.map(t=>t.id===n.id?{...t,answer:e===`delta`?t.answer+(n.text??``):e===`reset`?``:n.answer??t.answer,state:n.state??t.state,error:n.error===void 0?t.error:n.error}:t)}:r)});return()=>{n=!1,c.abort(),f.close()}},[t,k,e,x,C]),(0,g.useEffect)(()=>{if(!N?.kind||N.id!==k)return;let e=()=>{document.visibilityState===`visible`&&x(`agents/chats/${k}/read`,h({ok:s()}),{method:`POST`,body:{cursor:N.readCursor}}).then(()=>{D.reload(),window.dispatchEvent(new Event(`teammy:outbox`))}).catch(e=>U(i(e)))},t=setTimeout(e,300);return document.addEventListener(`visibilitychange`,e),()=>{clearTimeout(t),document.removeEventListener(`visibilitychange`,e)}},[k,N?.readCursor,N?.kind,x]),(0,g.useEffect)(()=>{q.current&&me.current?.scrollIntoView({block:`end`})},[O,j?.messages,Z?.recipients,X.length]);function he(){window.dispatchEvent(new Event(`teammy:outbox`)),E.reload()}async function $(e){if(q.current=!0,Y(e),U(``),P(void 0),e.startsWith(`chat:`)||e===`debug`)return;let t=E.data?.messages.find(t=>t.id===e)?.conversationId;if(t)Y(`chat:${t}`);else try{await x(`agents/outbox/${e}/read`,m,{method:`POST`}),he()}catch(e){U(i(e))}}async function ge(){if(!V){H(!0),U(``);try{let e=await x(`agents/chats`,p,{method:`POST`});M(e),await $(`chat:${e.id}`),D.reload()}catch(e){U(i(e))}finally{H(!1)}}}async function _e(e){e.preventDefault();let t=L.trim();if(!t||z||V)return;B(!0),U(``);let n=K.current.get(O);n&&n.text===t||(n={text:t,id:crypto.randomUUID()},K.current.set(O,n));try{if(A){let e=n.target??k;if(!e){let r=await x(`agents/chats`,p,{method:`POST`});e=r.id,n.target=e,K.current.set(`chat:${e}`,n),I(n=>({...n,[`chat:${e}`]:t})),J.current===O&&(M(r),Y(`chat:${e}`))}let i=await x(`agents/chats/${e}`,d.extend({conversationId:r().optional()}),{method:`POST`,body:{text:t,requestId:n.id}});i.conversationId&&i.conversationId!==e&&(J.current===O||J.current===`chat:${e}`)&&Y(`chat:${i.conversationId}`),M(t=>t?.id===e?{...t,runs:t.runs.some(e=>e.id===i.id)?t.runs:[...t.runs,i]}:t),D.reload()}else{let e=await x(`agents/outbox/${Z.id}/reply`,h({conversationId:r()}),{method:`POST`,body:{body:t,requestId:n.id}});he(),J.current===O&&Y(`chat:${e.conversationId}`),D.reload(),P(void 0)}K.current.delete(O),n.target&&K.current.delete(`chat:${n.target}`),I(e=>({...e,[O]:``,...n.target?{[`chat:${n.target}`]:``}:{}}))}catch(e){U(i(e))}finally{B(!1)}}let ve=A||!!Z&&!Z.archivedAt,ye=A&&!N&&(!!k||!D.data);return(0,_.jsxs)(`div`,{className:`split-layout assistant-layout`,"data-testid":`assistant-page`,children:[(0,_.jsxs)(`aside`,{className:`assistant-list`,"aria-label":`消息列表`,children:[(0,_.jsxs)(`header`,{className:`assistant-list-heading`,children:[(0,_.jsx)(`h1`,{children:`消息`}),(0,_.jsx)(oe,{className:`assistant-new-chat`,icon:`plus`,label:`新建对话`,disabled:V,onClick:()=>void ge(),"data-testid":`assistant-launcher`})]}),(0,_.jsxs)(`div`,{className:`assistant-message-scroll`,children:[D.loading&&!D.data&&(0,_.jsx)(`p`,{className:`assistant-list-status`,role:`status`,children:`加载中…`}),D.data?.chats.map(e=>(0,_.jsxs)(`button`,{type:`button`,"data-testid":`assistant-chat-${e.id}`,className:`assistant-message-item assistant-chat-item ${k===e.id?`selected`:``}`,"aria-pressed":k===e.id,onClick:()=>void $(`chat:${e.id}`),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${e.unread||e.state===`running`||e.state===`queued`?`unread`:``}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:k===e.id,children:(0,_.jsx)(`strong`,{children:e.title})}),(0,_.jsxs)(`time`,{dateTime:e.updatedAt,children:[new Date(e.updatedAt).toLocaleDateString([],{month:`numeric`,day:`numeric`}),` `,v(e.updatedAt)]})]})]},e.id)),!!X.length&&(0,_.jsxs)(`button`,{type:`button`,"data-testid":`debug-outbox`,className:`assistant-message-item ${O===`debug`?`selected`:``}`,onClick:()=>void $(`debug`),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${Q?`unread`:``}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:O===`debug`,children:(0,_.jsx)(`strong`,{children:`运行提醒`})}),(0,_.jsx)(`span`,{children:y(X.at(-1).body)})]})]}),E.data?.messages.filter(e=>e.box!==`debug`&&!e.conversationId).sort((e,t)=>Number(!!e.archivedAt)-Number(!!t.archivedAt)).map(e=>(0,_.jsxs)(`button`,{type:`button`,"data-testid":`outbox-${e.id}`,className:`assistant-message-item ${e.archivedAt?`archived`:``} ${O===e.id?`selected`:``}`,onClick:()=>void $(e.id),children:[(0,_.jsx)(`span`,{className:`assistant-message-dot ${e.readAt?``:`unread`}`}),(0,_.jsxs)(`span`,{className:`assistant-message-copy`,children:[(0,_.jsx)(u,{selected:O===e.id,children:(0,_.jsx)(`strong`,{children:y(e.body)})}),(0,_.jsxs)(`time`,{dateTime:e.createdAt,children:[new Date(e.createdAt).toLocaleDateString([],{month:`numeric`,day:`numeric`}),` `,v(e.createdAt)]})]})]},e.id))]}),(0,_.jsx)(o,{error:!0,children:E.error||D.error})]}),(0,_.jsxs)(`section`,{className:`assistant-thread`,"aria-label":A?`Tim对话`:`消息内容`,children:[(0,_.jsxs)(`header`,{className:`assistant-thread-heading`,children:[(0,_.jsx)(`h2`,{children:A?de?.title??N?.title??(k?`无标题`:`Tim`):O===`debug`?`运行提醒`:`项目消息`}),N?.kind===`matter`&&(0,_.jsxs)(`span`,{className:`assistant-participants`,title:N.participants?.join(`、`),children:[N.visibility===`project`?`项目共享 · `:``,N.participants?.join(`、`)]})]}),N?.kind===`matter`&&(N.hold||N.pending||N.status===`completed`)&&(0,_.jsx)(`div`,{className:`assistant-matter-state`,role:`status`,children:N.hold?`${N.hold.scope===`all`?`已暂停`:`暂缓修改`} · ${N.hold.reason}`:N.pending?`待确认 · ${N.pending.members.join(`、`)}:${N.pending.question}`:`已完成`}),(0,_.jsxs)(`div`,{className:`assistant-transcript`,role:`log`,"aria-live":`polite`,"data-testid":`assistant-transcript`,onScroll:e=>{let t=e.currentTarget;q.current=t.scrollHeight-t.scrollTop-t.clientHeight<100},children:[A?ye?(0,_.jsxs)(`div`,{className:`assistant-empty`,"data-testid":`assistant-loading`,children:[(0,_.jsx)(l,{}),(0,_.jsx)(`p`,{role:`status`,children:W||D.error?`暂时无法加载对话`:`加载中…`})]}):N?.messages.length?N.messages.filter(e=>e.body||[`queued`,`running`,`failed`].includes(e.state)).map(t=>(0,_.jsx)(`div`,{className:`assistant-turn`,children:(0,_.jsxs)(b,{mine:t.role===`user`&&t.author===e,author:`${t.role===`assistant`?`Tim`:t.author}${t.source===`wecom`?` · 企业微信`:``}`,time:t.createdAt,children:[t.body&&(0,_.jsx)(`div`,{className:`markdown`,children:(0,_.jsx)(se,{children:t.body})}),t.role===`assistant`&&[`queued`,`running`].includes(t.state)&&(0,_.jsxs)(`span`,{className:`assistant-typing`,"aria-label":`正在回答`,children:[(0,_.jsx)(`i`,{}),(0,_.jsx)(`i`,{}),(0,_.jsx)(`i`,{})]}),t.state===`failed`&&(0,_.jsx)(o,{error:!0,children:N.runs.find(e=>e.id===t.runId)?.error||`本次回答未完成。`})]})},t.id)):(0,_.jsxs)(`div`,{className:`assistant-empty`,children:[(0,_.jsx)(l,{}),(0,_.jsx)(`p`,{children:`有什么项目问题想聊聊?`})]}):!E.data&&E.loading?(0,_.jsx)(`div`,{className:`assistant-empty`,children:(0,_.jsx)(`p`,{role:`status`,children:`加载中…`})}):O===`debug`?X.map(e=>(0,_.jsx)(S,{item:e,onReply:e.archivedAt?void 0:()=>{P(e.id),R(``),U(``)}},e.id)):Z&&(0,_.jsx)(S,{item:Z}),(0,_.jsx)(`div`,{ref:me})]}),(0,_.jsx)(o,{error:!0,children:pe||(A?W:``)}),ve?(0,_.jsxs)(`form`,{className:`assistant-composer`,onSubmit:e=>void _e(e),children:[(0,_.jsx)(`textarea`,{value:L,disabled:V,onChange:e=>R(e.target.value),"aria-label":A?`向Tim提问`:`回复消息`,placeholder:A?`问问Tim…`:`回复…`,rows:2,onKeyDown:e=>{e.key===`Enter`&&!e.shiftKey&&!e.nativeEvent.isComposing&&(e.preventDefault(),e.currentTarget.form?.requestSubmit())}}),(0,_.jsx)(`button`,{className:`primary assistant-send`,type:`submit`,disabled:z||V||ye||!L.trim(),"aria-label":`发送`,children:(0,_.jsx)(ae,{name:`send`})})]}):O!==`debug`&&Z&&(0,_.jsxs)(`div`,{className:`assistant-archived`,children:[(0,_.jsx)(ae,{name:`archive`}),`已归档`]})]})]})}function S({item:e,onReply:t}){return(0,_.jsx)(`div`,{"data-testid":`outbox-thread-${e.id}`,children:(0,_.jsxs)(b,{author:e.box===`debug`?`运行提醒`:`Tim`,time:e.createdAt,children:[(0,_.jsx)(`div`,{className:`markdown`,children:(0,_.jsx)(se,{children:e.body})}),t&&(0,_.jsx)(`button`,{type:`button`,className:`assistant-debug-reply`,onClick:t,children:`回复`})]})})}export{x as Assistant};