@baishuyun/chat-backend 0.0.16 → 0.0.18

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.
Files changed (43) hide show
  1. package/.env.example +4 -1
  2. package/CHANGELOG.md +22 -0
  3. package/config/default.ts +9 -0
  4. package/dist/config/default.js +7 -0
  5. package/dist/src/app/main.js +4 -0
  6. package/dist/src/config/cache.config.js +6 -0
  7. package/dist/src/controllers/agent/bots.controller.js +29 -0
  8. package/dist/src/controllers/common/connect.controll.js +25 -0
  9. package/dist/src/controllers/common/model.js +10 -0
  10. package/dist/src/controllers/form/build/build.controller.js +28 -33
  11. package/dist/src/controllers/form/build/model.js +8 -0
  12. package/dist/src/controllers/form/build/utils.js +28 -0
  13. package/dist/src/controllers/form/fill/createFieldsFillingResultTransformStream.js +3 -6
  14. package/dist/src/controllers/form/fill/fill.controller.js +1 -13
  15. package/dist/src/controllers/form/fill/utils.js +1 -0
  16. package/dist/src/controllers/report/query/createQueryTransformStream.js +9 -0
  17. package/dist/src/controllers/report/query/query.controller.js +1 -16
  18. package/dist/src/controllers/report/query/suggest.controller.js +0 -4
  19. package/dist/src/middleware/tokenExchange.js +23 -0
  20. package/dist/src/routes/agent/agent.route.js +8 -0
  21. package/dist/src/routes/common/common.route.js +8 -0
  22. package/dist/src/services/fetchCozeInfo.js +26 -0
  23. package/dist/src/types/coze.js +1 -0
  24. package/dist/src/utils/createJsonStreamTransformer.js +3 -10
  25. package/dist/src/utils/safeJsonParser.js +8 -0
  26. package/package.json +6 -4
  27. package/src/app/main.ts +4 -0
  28. package/src/config/cache.config.ts +9 -0
  29. package/src/controllers/agent/bots.controller.ts +39 -0
  30. package/src/controllers/common/connect.controll.ts +32 -0
  31. package/src/controllers/common/model.ts +12 -0
  32. package/src/controllers/form/build/build.controller.ts +31 -28
  33. package/src/controllers/form/build/model.ts +10 -0
  34. package/src/controllers/form/build/utils.ts +39 -0
  35. package/src/controllers/form/fill/utils.ts +1 -0
  36. package/src/controllers/report/query/query.controller.ts +0 -3
  37. package/src/controllers/report/query/suggest.controller.ts +0 -4
  38. package/src/middleware/tokenExchange.ts +26 -0
  39. package/src/routes/agent/agent.route.ts +11 -0
  40. package/src/routes/common/common.route.ts +11 -0
  41. package/src/services/fetchCozeInfo.ts +33 -0
  42. package/src/types/coze.ts +13 -0
  43. package/src/utils/safeJsonParser.ts +7 -0
package/.env.example CHANGED
@@ -20,4 +20,7 @@ FILL_BOT_ID=xxxxx
20
20
  QUERY_BOT_ID=xxxxx
21
21
 
22
22
  # query suggest bot id
23
- QUERY_SUGGEST_BOT_ID=xxxx
23
+ QUERY_SUGGEST_BOT_ID=xxxx
24
+
25
+ # deepseek api key
26
+ DS_API_KEY=sk-xxxx
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @baishuyun/chat-backend
2
2
 
3
+ ## 0.0.18
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [e5dd21a]
8
+ - Updated dependencies [a934bc0]
9
+ - Updated dependencies
10
+ - Updated dependencies [4acb2cc]
11
+ - Updated dependencies [35058a9]
12
+ - @baishuyun/coze-provider@0.1.0
13
+ - @baishuyun/agents@0.1.0
14
+ - @baishuyun/types@1.1.0
15
+
16
+ ## 0.0.17
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+ - @baishuyun/coze-provider@0.0.17
22
+ - @baishuyun/agents@0.0.17
23
+ - @baishuyun/types@1.0.17
24
+
3
25
  ## 0.0.16
4
26
 
5
27
  ### Patch Changes
package/config/default.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { deepseek } from '@ai-sdk/deepseek';
1
2
  import { asyncConfig } from 'config/async.js';
2
3
  // load async configurations
3
4
  const fetchRemoteConfig = async () => {
@@ -18,11 +19,19 @@ export default {
18
19
 
19
20
  apiAuthKey: process.env.COZE_API_KEY,
20
21
 
22
+ userId: process.env.BUILTIN_COZE_USER_ID,
23
+
24
+ common: {
25
+ baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
26
+ apiKey: process.env.BOT_API_KEY, // load from env
27
+ },
28
+
21
29
  form: {
22
30
  build: {
23
31
  botId: process.env.BUILD_BOT_ID || '7579927677256073216',
24
32
  baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
25
33
  apiKey: process.env.BOT_API_KEY, // load from env
34
+ intentBotId: process.env.FORM_BUILD_INTENT_BOT_ID || '7615799745042186240',
26
35
  },
27
36
 
28
37
  fill: {
@@ -1,3 +1,4 @@
1
+ import { deepseek } from '@ai-sdk/deepseek';
1
2
  import { asyncConfig } from 'config/async.js';
2
3
  // load async configurations
3
4
  const fetchRemoteConfig = async () => {
@@ -14,11 +15,17 @@ export default {
14
15
  agent: {
15
16
  host: process.env.AGENT_HOST || '47.99.202.157',
16
17
  apiAuthKey: process.env.COZE_API_KEY,
18
+ userId: process.env.BUILTIN_COZE_USER_ID,
19
+ common: {
20
+ baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
21
+ apiKey: process.env.BOT_API_KEY, // load from env
22
+ },
17
23
  form: {
18
24
  build: {
19
25
  botId: process.env.BUILD_BOT_ID || '7579927677256073216',
20
26
  baseUrl: `http://${process.env.AGENT_HOST}/v3/`,
21
27
  apiKey: process.env.BOT_API_KEY, // load from env
28
+ intentBotId: process.env.FORM_BUILD_INTENT_BOT_ID || '7615799745042186240',
22
29
  },
23
30
  fill: {
24
31
  botId: process.env.FILL_BOT_ID || '7586483957357608960',
@@ -3,12 +3,16 @@
3
3
  */
4
4
  // 应用实例
5
5
  import app from '../config/hono.config.js';
6
+ import { createAgentRouter } from '../routes/agent/agent.route.js';
7
+ import { createCommonRouter } from '../routes/common/common.route.js';
6
8
  // 子路由
7
9
  import { createFormRouter } from '../routes/form/form.route.js';
8
10
  import { createReportRouter } from '../routes/report/report.route.js';
9
11
  // 挂载子路由
10
12
  app.route('web/api/form', createFormRouter());
11
13
  app.route('web/api/report', createReportRouter());
14
+ app.route('web/api/common', createCommonRouter());
15
+ app.route('web/api/agent', createAgentRouter());
12
16
  // 基础健康检查
13
17
  app.get('/web/api/health', (c) => {
14
18
  return c.json({
@@ -0,0 +1,6 @@
1
+ import { LRUCache } from 'lru-cache';
2
+ const options = {
3
+ max: 5000,
4
+ ttl: 1000 * 60 * 55, // 假设 Token 1 小时有效,我们存 55 分钟
5
+ };
6
+ export const tokenCache = new LRUCache(options);
@@ -0,0 +1,29 @@
1
+ import config from 'config';
2
+ import { logger } from '../../logger/index.js';
3
+ import { safeJsonParser } from '../../utils/safeJsonParser.js';
4
+ export const listBots = async (c) => {
5
+ const agentHost = config.get('agent.host');
6
+ const apiBots = `http://${agentHost}/v1/bots`;
7
+ const cozeInfo = c.get('X-Coze-Info');
8
+ logger.debug(`Fetching bots from ${apiBots} with cozeToken: ${cozeInfo.cozeToken}`);
9
+ const result = await fetch(apiBots, {
10
+ method: 'GET',
11
+ headers: {
12
+ Authorization: `Bearer ${cozeInfo.cozeToken}`,
13
+ },
14
+ });
15
+ const resJson = await result.json();
16
+ logger.debug(JSON.stringify(resJson, null, 2));
17
+ if (!resJson.bot_list || !Array.isArray(resJson.bot_list)) {
18
+ return c.json([]);
19
+ }
20
+ const minifyedBots = resJson.bot_list.map((bot) => ({
21
+ id: bot.bot_id,
22
+ name: bot.name,
23
+ description: bot.description,
24
+ icon: bot.icon_url,
25
+ lastEditTime: bot.update_time,
26
+ publisherConfig: safeJsonParser(bot.pre_publish_ext),
27
+ }));
28
+ return c.json(minifyedBots);
29
+ };
@@ -0,0 +1,25 @@
1
+ import { convertToModelMessages, streamText } from 'ai';
2
+ import { createBaseModel } from './model.js';
3
+ export const connectToAgent = async (c) => {
4
+ let requestBody;
5
+ try {
6
+ const json = await c.req.json();
7
+ requestBody = json;
8
+ }
9
+ catch (_) {
10
+ return c.json({ error: 'Invalid JSON' }, 400);
11
+ }
12
+ const cozeInfo = c.get('X-Coze-Info');
13
+ const messages = requestBody.messages;
14
+ const botId = c.req.header('X-Bot-Id') || '';
15
+ const stream = streamText({
16
+ model: createBaseModel(botId, cozeInfo.cozeToken),
17
+ messages: convertToModelMessages(messages),
18
+ includeRawChunks: true,
19
+ headers: {
20
+ 'x-user-var': requestBody.userVar,
21
+ 'x-user-id': cozeInfo.userId,
22
+ },
23
+ });
24
+ return stream.toUIMessageStreamResponse();
25
+ };
@@ -0,0 +1,10 @@
1
+ import { createCoze } from '@baishuyun/coze-provider';
2
+ import config from 'config';
3
+ export const createBaseModel = (botId, token) => {
4
+ const coze = createCoze({
5
+ apiKey: token || config.get('agent.common.apiKey'),
6
+ baseURL: config.get('agent.common.baseUrl'),
7
+ botId: botId,
8
+ });
9
+ return coze.chat('chat');
10
+ };
@@ -1,7 +1,8 @@
1
- import { convertToModelMessages, streamText } from "ai";
1
+ import { convertToModelMessages, streamText, generateText } from "ai";
2
2
  import {} from "hono";
3
3
  import { createModel } from "./model.js";
4
4
  import { createFieldsJsonTransformStream, SuggestionTransformStream, } from "@baishuyun/coze-provider";
5
+ import { determineUserIntentByInput } from "./utils.js";
5
6
  /**
6
7
  * 搭建表单
7
8
  * @param c
@@ -16,44 +17,38 @@ export const buildForm = async (c) => {
16
17
  catch (_) {
17
18
  return c.json({ error: "Invalid JSON" }, 400);
18
19
  }
19
- const isBuildStage = requestBody.stage === "build";
20
- const formName = requestBody.name || "未命名表单";
21
- const allMessages = [...requestBody.messages];
22
- if (isBuildStage) {
23
- allMessages.push({
24
- role: "user",
25
- parts: [
26
- {
27
- type: "text",
28
- text: `确认搭建:${formName}`,
29
- },
30
- ],
20
+ const intent = await determineUserIntentByInput(requestBody.text, requestBody.stage);
21
+ const isBuildStage = intent === "build";
22
+ const formName = requestBody.name;
23
+ const model = createModel([
24
+ () => createFieldsJsonTransformStream(isBuildStage),
25
+ () => new SuggestionTransformStream(isBuildStage),
26
+ ]);
27
+ const allMsg = requestBody.messages || [];
28
+ const lastUserMsg = allMsg.length > 0 ? allMsg[allMsg.length - 1] : {
29
+ role: "user",
30
+ parts: [],
31
+ };
32
+ if (isBuildStage && formName) {
33
+ lastUserMsg.parts = lastUserMsg.parts.map((p, index) => {
34
+ const isLastPart = index === lastUserMsg.parts.length - 1;
35
+ if (!isLastPart || p.type !== "text") {
36
+ return p;
37
+ }
38
+ return {
39
+ type: "text",
40
+ text: `【确认搭建:${formName}】 ${p.text}`
41
+ };
31
42
  });
32
43
  }
33
44
  const stream = streamText({
34
- model: createModel([
35
- () => createFieldsJsonTransformStream(isBuildStage),
36
- () => new SuggestionTransformStream(isBuildStage),
37
- ]),
38
- messages: convertToModelMessages(allMessages),
45
+ model,
46
+ messages: convertToModelMessages([lastUserMsg]),
39
47
  includeRawChunks: true,
40
48
  headers: {
41
- "x-user-stage": requestBody.stage,
42
- "x-user-id": Date.now().toString(),
49
+ "x-user-stage": intent,
43
50
  "x-user-token": c.req.header("authorization") || "",
44
51
  },
45
52
  });
46
- const response = stream.toUIMessageStreamResponse();
47
- // Add SSE keep-alive headers to prevent proxy timeouts during streaming
48
- return new Response(response.body, {
49
- status: response.status,
50
- statusText: response.statusText,
51
- headers: {
52
- ...Object.fromEntries(response.headers),
53
- 'Connection': 'keep-alive',
54
- 'Keep-Alive': 'timeout=300',
55
- 'X-Accel-Buffering': 'no',
56
- 'Cache-Control': 'no-cache, no-transform',
57
- },
58
- });
53
+ return stream.toUIMessageStreamResponse();
59
54
  };
@@ -9,3 +9,11 @@ export const createModel = (extraStreamTransformers) => {
9
9
  });
10
10
  return coze.chat("chat");
11
11
  };
12
+ export const createBuildIntentAgent = () => {
13
+ const coze = createCoze({
14
+ apiKey: config.get("agent.form.build.apiKey"),
15
+ baseURL: config.get("agent.form.build.baseUrl"),
16
+ botId: config.get("agent.form.build.intentBotId"),
17
+ });
18
+ return coze.chat("chat");
19
+ };
@@ -0,0 +1,28 @@
1
+ import { logger } from "../../../logger/index.js";
2
+ import { createBuildIntentAgent } from "./model.js";
3
+ export const determineUserIntentByInput = async (input, userOriginIntent) => {
4
+ if (userOriginIntent === "design") {
5
+ return "design";
6
+ }
7
+ if (!input || input.trim() === "") {
8
+ return userOriginIntent;
9
+ }
10
+ const cozeAgent = createBuildIntentAgent();
11
+ const intent = await cozeAgent.doGenerate({
12
+ prompt: [{
13
+ role: "user",
14
+ content: [{
15
+ type: "text",
16
+ text: input,
17
+ }]
18
+ }]
19
+ });
20
+ logger.debug("agent intent response: " + JSON.stringify(intent, null, 2));
21
+ const result = intent.content;
22
+ if (!result || result.length === 0) {
23
+ return userOriginIntent;
24
+ }
25
+ const lastContent = result[result.length - 1];
26
+ const intentText = lastContent.text === "build" ? "build" : lastContent.text === "design" ? "design" : userOriginIntent;
27
+ return intentText;
28
+ };
@@ -54,14 +54,13 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
54
54
  }
55
55
  catch (e) {
56
56
  logger.debug('exception in transform while processing filling chunk');
57
- // Enqueue error message before closing to allow client to receive it
57
+ controller.error(e);
58
58
  enqueueTextDelta('error', {
59
59
  type: 'agent-error',
60
60
  error: '解析异常,请刷新重试',
61
61
  });
62
62
  resolveParseCompleted?.();
63
- // Graceful close after error notification
64
- controller.terminate();
63
+ controller.terminate(); // 信号通知下游可读流已关闭
65
64
  }
66
65
  },
67
66
  start: (controller) => {
@@ -107,14 +106,12 @@ export const createFieldsFillingResultTransformer = (enableJsonParser) => {
107
106
  console.error('JsonWidgetStream: JSON Parsing Error:', err);
108
107
  errorLogged = true;
109
108
  }
110
- // Enqueue error message before closing to allow client to receive it
111
109
  enqueueTextDelta('error', {
112
110
  type: 'agent-error',
113
111
  error: '操作超时,请刷新重试',
114
112
  });
115
113
  resolveParseCompleted?.();
116
- // Graceful close after error notification
117
- controller.terminate();
114
+ controller.terminate(); // 信号通知下游可读流已关闭
118
115
  };
119
116
  parser.onEnd = () => {
120
117
  enqueueTextDelta(' ', JSON.stringify({
@@ -61,19 +61,7 @@ export const fillForm = async (c) => {
61
61
  'x-user-token': c.req.header('authorization') || '',
62
62
  },
63
63
  });
64
- const response = stream.toUIMessageStreamResponse({
64
+ return stream.toUIMessageStreamResponse({
65
65
  originalMessages: messages, // 建议添加,便于消息 ID 管理
66
66
  });
67
- // Add SSE keep-alive headers to prevent proxy timeouts during streaming
68
- return new Response(response.body, {
69
- status: response.status,
70
- statusText: response.statusText,
71
- headers: {
72
- ...Object.fromEntries(response.headers),
73
- 'Connection': 'keep-alive',
74
- 'Keep-Alive': 'timeout=300',
75
- 'X-Accel-Buffering': 'no',
76
- 'Cache-Control': 'no-cache, no-transform',
77
- },
78
- });
79
67
  };
@@ -104,6 +104,7 @@ export function trimFormStructure(fields) {
104
104
  widget: {
105
105
  type: item.widget.type,
106
106
  widgetName: item.widget.widgetName,
107
+ noRepeat: item.widget.noRepeat, // 保留 noRepeat 属性
107
108
  },
108
109
  }));
109
110
  }
@@ -48,10 +48,18 @@ function handleParsedValue(ctx) {
48
48
  const { parsedInfo, getResult, currentChunkId, deltaChunkEnqueuer: enqueueTextDelta, ctrl } = ctx;
49
49
  const { value } = parsedInfo;
50
50
  logger.debug('Parsed JSON value: ' + JSON.stringify(value));
51
+ // aggregate 类型 —— name 字段触发,独立处理后直接返回
52
+ if (isTargetElement('$.name', parsedInfo)) {
53
+ handleAggregateName(ctx);
54
+ return;
55
+ }
51
56
  // Phase 0: 报表标题 —— 初始化结果并发送 text-start, 必须为起始字段
52
57
  if (isTargetElement('$.title', parsedInfo)) {
53
58
  handleTitle(ctx);
54
59
  }
60
+ // Phase 3: aggregate 结果无需后续字段处理
61
+ if (getResult().isAggregate)
62
+ return;
55
63
  // Phase 4: 按注册的 handler 分发字段处理
56
64
  for (const { path, handler } of fieldHandlers) {
57
65
  if (isTargetElement(path, parsedInfo)) {
@@ -70,6 +78,7 @@ function handleAggregateName(ctx) {
70
78
  title: value,
71
79
  source: 'aggregate',
72
80
  type: 'data_table',
81
+ isAggregate: true,
73
82
  });
74
83
  logger.debug('Parsed aggregate table title: ' + value);
75
84
  enqueueTextDelta(`${JSON.stringify(value)},`, { type: 'query-stream-parsed-info', result: JSON.stringify(getResult()) }, currentChunkId + 1, false);
@@ -32,23 +32,8 @@ export const queryReport = async (c) => {
32
32
  model: createReportQueryModel(),
33
33
  messages: modelMessages,
34
34
  includeRawChunks: true,
35
- headers: {
36
- 'x-user-id': Date.now().toString(), // uid,
37
- },
38
35
  });
39
- const response = stream.toUIMessageStreamResponse({
36
+ return stream.toUIMessageStreamResponse({
40
37
  originalMessages: messages, // 建议添加,便于消息 ID 管理
41
38
  });
42
- // Add SSE keep-alive headers to prevent proxy timeouts during streaming
43
- return new Response(response.body, {
44
- status: response.status,
45
- statusText: response.statusText,
46
- headers: {
47
- ...Object.fromEntries(response.headers),
48
- 'Connection': 'keep-alive',
49
- 'Keep-Alive': 'timeout=300',
50
- 'X-Accel-Buffering': 'no',
51
- 'Cache-Control': 'no-cache, no-transform',
52
- },
53
- });
54
39
  };
@@ -10,7 +10,6 @@ export const queryReportSuggest = async (c) => {
10
10
  catch (_) {
11
11
  return c.json({ error: 'Invalid JSON' }, 400);
12
12
  }
13
- const uid = c.req.header('X-User-Id') || '';
14
13
  const messages = requestBody.messages;
15
14
  const forms = requestBody.forms || [];
16
15
  const modelMessages = convertToModelMessages([
@@ -29,9 +28,6 @@ export const queryReportSuggest = async (c) => {
29
28
  model: createReportQueryModel(),
30
29
  messages: modelMessages,
31
30
  includeRawChunks: true,
32
- headers: {
33
- 'x-user-id': uid,
34
- },
35
31
  });
36
32
  return stream.toUIMessageStreamResponse({
37
33
  originalMessages: messages, // 建议添加,便于消息 ID 管理
@@ -0,0 +1,23 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { tokenCache } from '../config/cache.config.js';
3
+ import { fetchCozeInfo } from '../services/fetchCozeInfo.js';
4
+ import { logger } from '../logger/index.js';
5
+ export const tokenExchange = () => createMiddleware(async (c, next) => {
6
+ const userAuth = c.req.header('X-Bs-Team-Id') || c.req.query('x-bs-team-id') || '';
7
+ if (!userAuth) {
8
+ return c.json({ error: 'Unauthorized' }, 401);
9
+ }
10
+ let cozeInfo = tokenCache.get(userAuth);
11
+ if (!cozeInfo) {
12
+ try {
13
+ cozeInfo = await fetchCozeInfo(userAuth);
14
+ tokenCache.set(userAuth, cozeInfo);
15
+ }
16
+ catch (error) {
17
+ logger.error(`Failed to exchange token for userAuth: ${userAuth}, error: ${error}`);
18
+ return c.json({ error: 'Failed to exchange token' }, 500);
19
+ }
20
+ }
21
+ c.set('X-Coze-Info', cozeInfo);
22
+ await next();
23
+ });
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+ import { listBots } from '../../controllers/agent/bots.controller.js';
3
+ import { tokenExchange } from '../../middleware/tokenExchange.js';
4
+ export const createAgentRouter = () => {
5
+ const agentRouter = new Hono();
6
+ agentRouter.get('/bots', tokenExchange(), listBots);
7
+ return agentRouter;
8
+ };
@@ -0,0 +1,8 @@
1
+ import { Hono } from 'hono';
2
+ import { connectToAgent } from '../../controllers/common/connect.controll.js';
3
+ import { tokenExchange } from '../../middleware/tokenExchange.js';
4
+ export const createCommonRouter = () => {
5
+ const commonRouter = new Hono();
6
+ commonRouter.post('/connect', tokenExchange(), connectToAgent);
7
+ return commonRouter;
8
+ };
@@ -0,0 +1,26 @@
1
+ import config from 'config';
2
+ import { logger } from '../logger/index.js';
3
+ export const fetchCozeInfo = async (bsTeamId) => {
4
+ const agentHost = config.get('agent.host');
5
+ const apiUrl = `http://${agentHost}/api/by_teamid/tokens`;
6
+ const res = await fetch(apiUrl, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ body: JSON.stringify({ token: bsTeamId }),
12
+ });
13
+ const result = await res.json();
14
+ if (result.code !== 0) {
15
+ throw new Error('Failed to fetch Coze token');
16
+ }
17
+ const tokenList = result.data;
18
+ if (tokenList.length === 0) {
19
+ throw new Error('No Coze token returned');
20
+ }
21
+ const cozeToken = tokenList[0];
22
+ return {
23
+ cozeToken,
24
+ userId: `${result.team_uid}`,
25
+ };
26
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -83,8 +83,8 @@ class JsonStreamProcessor {
83
83
  await this.parseCompleted;
84
84
  logger.debug('Parser stopped gracefully');
85
85
  }
86
- // Graceful close: don't call terminate() which abruptly closes the stream
87
- // The stream will close naturally when all chunks are processed
86
+ if (!this.terminated)
87
+ controller.terminate();
88
88
  }
89
89
  catch (error) {
90
90
  controller.error(`Cleanup error: ${error}`);
@@ -160,14 +160,7 @@ class JsonStreamProcessor {
160
160
  terminateStream(controller) {
161
161
  this.completeParsing();
162
162
  this.terminated = true;
163
- // Use error() instead of terminate() to signal an error condition
164
- // This allows the stream to be properly handled by upstream consumers
165
- try {
166
- controller.error(new Error('Stream terminated due to error'));
167
- }
168
- catch (e) {
169
- // Controller may already be closed, ignore
170
- }
163
+ controller.terminate();
171
164
  }
172
165
  /** 向下游推送 agent-error 并终止流 */
173
166
  enqueueError(controller, error) {
@@ -0,0 +1,8 @@
1
+ export const safeJsonParser = (str) => {
2
+ try {
3
+ return JSON.parse(str);
4
+ }
5
+ catch (error) {
6
+ return null; // or you can choose to return an empty object {} or any default value
7
+ }
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/chat-backend",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,11 +19,13 @@
19
19
  "config": "^4.1.1",
20
20
  "dotenv": "^17.2.3",
21
21
  "hono": "^4.10.6",
22
+ "lru-cache": "^11.2.7",
22
23
  "parse-sse": "^0.1.0",
23
24
  "pino": "^10.1.0",
24
25
  "zod": "^4.1.13",
25
- "@baishuyun/coze-provider": "0.0.16",
26
- "@baishuyun/types": "1.0.16"
26
+ "@baishuyun/coze-provider": "0.1.0",
27
+ "@baishuyun/types": "1.1.0",
28
+ "@baishuyun/agents": "0.1.0"
27
29
  },
28
30
  "devDependencies": {
29
31
  "@types/config": "^3.3.5",
@@ -34,7 +36,7 @@
34
36
  "pm2": "^6.0.14",
35
37
  "tsx": "^4.7.1",
36
38
  "typescript": "^5.8.3",
37
- "@baishuyun/typescript-config": "0.0.16"
39
+ "@baishuyun/typescript-config": "0.1.0"
38
40
  },
39
41
  "scripts": {
40
42
  "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
package/src/app/main.ts CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  // 应用实例
6
6
  import app from '../config/hono.config.js';
7
+ import { createAgentRouter } from '../routes/agent/agent.route.js';
8
+ import { createCommonRouter } from '../routes/common/common.route.js';
7
9
 
8
10
  // 子路由
9
11
  import { createFormRouter } from '../routes/form/form.route.js';
@@ -12,6 +14,8 @@ import { createReportRouter } from '../routes/report/report.route.js';
12
14
  // 挂载子路由
13
15
  app.route('web/api/form', createFormRouter());
14
16
  app.route('web/api/report', createReportRouter());
17
+ app.route('web/api/common', createCommonRouter());
18
+ app.route('web/api/agent', createAgentRouter());
15
19
 
16
20
  // 基础健康检查
17
21
  app.get('/web/api/health', (c) => {
@@ -0,0 +1,9 @@
1
+ import { LRUCache } from 'lru-cache';
2
+ import type { ICozeInfoOfTokenExchange } from '../types/coze.js';
3
+
4
+ const options = {
5
+ max: 5000,
6
+ ttl: 1000 * 60 * 55, // 假设 Token 1 小时有效,我们存 55 分钟
7
+ };
8
+
9
+ export const tokenCache = new LRUCache<string, ICozeInfoOfTokenExchange>(options);
@@ -0,0 +1,39 @@
1
+ import type { Context } from 'hono';
2
+ import config from 'config';
3
+ import { logger } from '../../logger/index.js';
4
+ import { safeJsonParser } from '../../utils/safeJsonParser.js';
5
+ import type { ICozeInfoOfTokenExchange } from '../../types/coze.js';
6
+
7
+ export const listBots = async (c: Context) => {
8
+ const agentHost = config.get<string>('agent.host');
9
+ const apiBots = `http://${agentHost}/v1/bots`;
10
+ const cozeInfo = c.get('X-Coze-Info') as ICozeInfoOfTokenExchange;
11
+
12
+ logger.debug(`Fetching bots from ${apiBots} with cozeToken: ${cozeInfo.cozeToken}`);
13
+
14
+ const result = await fetch(apiBots, {
15
+ method: 'GET',
16
+ headers: {
17
+ Authorization: `Bearer ${cozeInfo.cozeToken}`,
18
+ },
19
+ });
20
+
21
+ const resJson = await result.json();
22
+
23
+ logger.debug(JSON.stringify(resJson, null, 2));
24
+
25
+ if (!resJson.bot_list || !Array.isArray(resJson.bot_list)) {
26
+ return c.json([]);
27
+ }
28
+
29
+ const minifyedBots = resJson.bot_list.map((bot: any) => ({
30
+ id: bot.bot_id,
31
+ name: bot.name,
32
+ description: bot.description,
33
+ icon: bot.icon_url,
34
+ lastEditTime: bot.update_time,
35
+ publisherConfig: safeJsonParser(bot.pre_publish_ext),
36
+ }));
37
+
38
+ return c.json(minifyedBots);
39
+ };
@@ -0,0 +1,32 @@
1
+ import { convertToModelMessages, streamText } from 'ai';
2
+ import type { Context } from 'hono';
3
+ import { createBaseModel } from './model.js';
4
+ import type { ICozeInfoOfTokenExchange } from '../../types/coze.js';
5
+
6
+ export const connectToAgent = async (c: Context) => {
7
+ let requestBody;
8
+ try {
9
+ const json = await c.req.json();
10
+ requestBody = json;
11
+ } catch (_) {
12
+ return c.json({ error: 'Invalid JSON' }, 400);
13
+ }
14
+
15
+ const cozeInfo = c.get('X-Coze-Info') as ICozeInfoOfTokenExchange;
16
+
17
+ const messages = requestBody.messages;
18
+
19
+ const botId = c.req.header('X-Bot-Id') || '';
20
+
21
+ const stream = streamText({
22
+ model: createBaseModel(botId, cozeInfo.cozeToken),
23
+ messages: convertToModelMessages(messages),
24
+ includeRawChunks: true,
25
+ headers: {
26
+ 'x-user-var': requestBody.userVar,
27
+ 'x-user-id': cozeInfo.userId,
28
+ },
29
+ });
30
+
31
+ return stream.toUIMessageStreamResponse();
32
+ };
@@ -0,0 +1,12 @@
1
+ import { createCoze } from '@baishuyun/coze-provider';
2
+ import config from 'config';
3
+
4
+ export const createBaseModel = (botId: string, token?: string) => {
5
+ const coze = createCoze({
6
+ apiKey: token || config.get<string>('agent.common.apiKey'),
7
+ baseURL: config.get<string>('agent.common.baseUrl'),
8
+ botId: botId,
9
+ });
10
+
11
+ return coze.chat('chat');
12
+ };
@@ -1,10 +1,11 @@
1
- import { convertToModelMessages, streamText, type TextUIPart } from "ai";
1
+ import { convertToModelMessages, streamText, generateText, type TextUIPart } from "ai";
2
2
  import { type Context } from "hono";
3
3
  import { createModel } from "./model.js";
4
4
  import {
5
5
  createFieldsJsonTransformStream,
6
6
  SuggestionTransformStream,
7
7
  } from "@baishuyun/coze-provider";
8
+ import { determineUserIntentByInput } from "./utils.js";
8
9
 
9
10
  /**
10
11
  * 搭建表单
@@ -21,39 +22,41 @@ export const buildForm = async (c: Context) => {
21
22
  return c.json({ error: "Invalid JSON" }, 400);
22
23
  }
23
24
 
24
- const isBuildStage = requestBody.stage === "build";
25
- const formName = requestBody.name || "未命名表单";
26
-
27
- const allMessages = [...requestBody.messages];
28
- if (isBuildStage) {
29
- allMessages.push({
30
- role: "user",
31
- parts: [
32
- {
33
- type: "text",
34
- text: `确认搭建:${formName}`,
35
- },
36
- ],
25
+ const intent = await determineUserIntentByInput(requestBody.text, requestBody.stage);
26
+
27
+ const isBuildStage = intent === "build";
28
+ const formName = requestBody.name;
29
+ const model = createModel([
30
+ () => createFieldsJsonTransformStream(isBuildStage),
31
+ () => new SuggestionTransformStream(isBuildStage),
32
+ ]);
33
+
34
+ const allMsg = requestBody.messages || [];
35
+ const lastUserMsg = allMsg.length > 0 ? allMsg[allMsg.length - 1] : {
36
+ role: "user",
37
+ parts: [],
38
+ };
39
+
40
+ if (isBuildStage && formName) {
41
+ lastUserMsg.parts = lastUserMsg.parts.map((p: any, index: number) => {
42
+ const isLastPart = index === lastUserMsg.parts.length - 1;
43
+ if (!isLastPart || p.type !== "text") {
44
+ return p;
45
+ }
46
+
47
+ return {
48
+ type: "text",
49
+ text: `【确认搭建:${formName}】 ${p.text}`
50
+ };
37
51
  });
38
52
  }
39
53
 
40
- // clear empty text parts to avoid unnecessary streaming
41
- allMessages.forEach((message) => {
42
- message.parts = message.parts.filter(
43
- (part: TextUIPart) => part.type === "text" && part.text.trim() !== ""
44
- );
45
- });
46
-
47
54
  const stream = streamText({
48
- model: createModel([
49
- () => createFieldsJsonTransformStream(isBuildStage),
50
- () => new SuggestionTransformStream(isBuildStage),
51
- ]),
52
- messages: convertToModelMessages(allMessages),
55
+ model,
56
+ messages: convertToModelMessages([lastUserMsg]),
53
57
  includeRawChunks: true,
54
58
  headers: {
55
- "x-user-stage": requestBody.stage,
56
- "x-user-id": Date.now().toString(),
59
+ "x-user-stage": intent as string,
57
60
  "x-user-token": c.req.header("authorization") || "",
58
61
  },
59
62
  });
@@ -14,3 +14,13 @@ export const createModel = (
14
14
 
15
15
  return coze.chat("chat");
16
16
  };
17
+
18
+ export const createBuildIntentAgent = () => {
19
+ const coze = createCoze({
20
+ apiKey: config.get<string>("agent.form.build.apiKey"),
21
+ baseURL: config.get<string>("agent.form.build.baseUrl"),
22
+ botId: config.get<string>("agent.form.build.intentBotId"),
23
+ });
24
+
25
+ return coze.chat("chat");
26
+ }
@@ -0,0 +1,39 @@
1
+ import { logger } from "../../../logger/index.js";
2
+ import { createBuildIntentAgent } from "./model.js"
3
+
4
+ export const determineUserIntentByInput = async (input: string, userOriginIntent: "build" | "design" | null): Promise<"build" | "design" | null> => {
5
+ if (userOriginIntent === "design") {
6
+ return "design";
7
+ }
8
+
9
+ if (!input || input.trim() === "") {
10
+ return userOriginIntent;
11
+ }
12
+
13
+ const cozeAgent = createBuildIntentAgent();
14
+ const intent = await cozeAgent.doGenerate({
15
+ prompt: [{
16
+ role: "user",
17
+ content: [{
18
+ type: "text",
19
+ text: input,
20
+ }]
21
+ }]
22
+ });
23
+
24
+ logger.debug("agent intent response: " + JSON.stringify(intent, null, 2));
25
+
26
+ const result = intent.content;
27
+ if (!result || result.length === 0) {
28
+ return userOriginIntent;
29
+ }
30
+
31
+ const lastContent = result[result.length - 1] as {
32
+ type: "text";
33
+ text: string;
34
+ };
35
+
36
+ const intentText = lastContent.text === "build" ? "build" : lastContent.text === "design" ? "design" : userOriginIntent;
37
+
38
+ return intentText;
39
+ }
@@ -166,6 +166,7 @@ export function trimFormStructure(fields: OriginalField[]): TrimmedField[] {
166
166
  widget: {
167
167
  type: item.widget.type,
168
168
  widgetName: item.widget.widgetName,
169
+ noRepeat: item.widget.noRepeat, // 保留 noRepeat 属性
169
170
  },
170
171
  }));
171
172
  }
@@ -39,9 +39,6 @@ export const queryReport = async (c: Context) => {
39
39
  model: createReportQueryModel(),
40
40
  messages: modelMessages,
41
41
  includeRawChunks: true,
42
- headers: {
43
- 'x-user-id': Date.now().toString(), // uid,
44
- },
45
42
  });
46
43
 
47
44
  return stream.toUIMessageStreamResponse({
@@ -12,7 +12,6 @@ export const queryReportSuggest = async (c: Context) => {
12
12
  return c.json({ error: 'Invalid JSON' }, 400);
13
13
  }
14
14
 
15
- const uid = c.req.header('X-User-Id') || '';
16
15
  const messages: UIMessage[] = requestBody.messages;
17
16
 
18
17
  const forms = requestBody.forms || [];
@@ -35,9 +34,6 @@ export const queryReportSuggest = async (c: Context) => {
35
34
  model: createReportQueryModel(),
36
35
  messages: modelMessages,
37
36
  includeRawChunks: true,
38
- headers: {
39
- 'x-user-id': uid,
40
- },
41
37
  });
42
38
 
43
39
  return stream.toUIMessageStreamResponse({
@@ -0,0 +1,26 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { tokenCache } from '../config/cache.config.js';
3
+ import { fetchCozeInfo } from '../services/fetchCozeInfo.js';
4
+ import { logger } from '../logger/index.js';
5
+
6
+ export const tokenExchange = () =>
7
+ createMiddleware(async (c, next) => {
8
+ const userAuth = c.req.header('X-Bs-Team-Id') || c.req.query('x-bs-team-id') || '';
9
+ if (!userAuth) {
10
+ return c.json({ error: 'Unauthorized' }, 401);
11
+ }
12
+
13
+ let cozeInfo = tokenCache.get(userAuth);
14
+ if (!cozeInfo) {
15
+ try {
16
+ cozeInfo = await fetchCozeInfo(userAuth);
17
+ tokenCache.set(userAuth, cozeInfo);
18
+ } catch (error) {
19
+ logger.error(`Failed to exchange token for userAuth: ${userAuth}, error: ${error}`);
20
+ return c.json({ error: 'Failed to exchange token' }, 500);
21
+ }
22
+ }
23
+
24
+ c.set('X-Coze-Info', cozeInfo);
25
+ await next();
26
+ });
@@ -0,0 +1,11 @@
1
+ import { Hono } from 'hono';
2
+ import { listBots } from '../../controllers/agent/bots.controller.js';
3
+ import { tokenExchange } from '../../middleware/tokenExchange.js';
4
+
5
+ export const createAgentRouter = () => {
6
+ const agentRouter = new Hono();
7
+
8
+ agentRouter.get('/bots', tokenExchange(), listBots);
9
+
10
+ return agentRouter;
11
+ };
@@ -0,0 +1,11 @@
1
+ import { Hono } from 'hono';
2
+ import { connectToAgent } from '../../controllers/common/connect.controll.js';
3
+ import { tokenExchange } from '../../middleware/tokenExchange.js';
4
+
5
+ export const createCommonRouter = () => {
6
+ const commonRouter = new Hono();
7
+
8
+ commonRouter.post('/connect', tokenExchange(), connectToAgent);
9
+
10
+ return commonRouter;
11
+ };
@@ -0,0 +1,33 @@
1
+ import config from 'config';
2
+ import type { ICozeInfoOfTokenExchange, OpenSpaceData } from '../types/coze.js';
3
+ import { logger } from '../logger/index.js';
4
+
5
+ export const fetchCozeInfo = async (bsTeamId: string): Promise<ICozeInfoOfTokenExchange> => {
6
+ const agentHost = config.get<string>('agent.host');
7
+
8
+ const apiUrl = `http://${agentHost}/api/by_teamid/tokens`;
9
+ const res = await fetch(apiUrl, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify({ token: bsTeamId }),
15
+ });
16
+
17
+ const result = await res.json();
18
+ if (result.code !== 0) {
19
+ throw new Error('Failed to fetch Coze token');
20
+ }
21
+
22
+ const tokenList = result.data as Array<string>;
23
+ if (tokenList.length === 0) {
24
+ throw new Error('No Coze token returned');
25
+ }
26
+
27
+ const cozeToken = tokenList[0];
28
+
29
+ return {
30
+ cozeToken,
31
+ userId: `${result.team_uid}`,
32
+ };
33
+ };
@@ -0,0 +1,13 @@
1
+ export interface ICozeInfoOfTokenExchange {
2
+ cozeToken: string;
3
+ userId: string;
4
+ }
5
+
6
+ export type OpenSpaceData = Array<IOpenSpace>;
7
+
8
+ export interface IOpenSpace {
9
+ id: string;
10
+ name: string;
11
+ icon_url: string;
12
+ owner_uid: string;
13
+ }
@@ -0,0 +1,7 @@
1
+ export const safeJsonParser = (str: string) => {
2
+ try {
3
+ return JSON.parse(str);
4
+ } catch (error) {
5
+ return null; // or you can choose to return an empty object {} or any default value
6
+ }
7
+ };