@baishuyun/chat-backend 0.0.16 → 0.0.17

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/.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,14 @@
1
1
  # @baishuyun/chat-backend
2
2
 
3
+ ## 0.0.17
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @baishuyun/coze-provider@0.0.17
9
+ - @baishuyun/agents@0.0.17
10
+ - @baishuyun/types@1.0.17
11
+
3
12
  ## 0.0.16
4
13
 
5
14
  ### 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,6 +19,13 @@ export default {
18
19
 
19
20
  apiAuthKey: process.env.COZE_API_KEY,
20
21
 
22
+ deepseekApiKey: process.env.DS_API_KEY,
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',
@@ -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,6 +15,11 @@ 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
+ deepseekApiKey: process.env.DS_API_KEY,
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',
@@ -3,12 +3,14 @@
3
3
  */
4
4
  // 应用实例
5
5
  import app from '../config/hono.config.js';
6
+ import { createCommonRouter } from '../routes/common/common.route.js';
6
7
  // 子路由
7
8
  import { createFormRouter } from '../routes/form/form.route.js';
8
9
  import { createReportRouter } from '../routes/report/report.route.js';
9
10
  // 挂载子路由
10
11
  app.route('web/api/form', createFormRouter());
11
12
  app.route('web/api/report', createReportRouter());
13
+ app.route('web/api/common', createCommonRouter());
12
14
  // 基础健康检查
13
15
  app.get('/web/api/health', (c) => {
14
16
  return c.json({
@@ -0,0 +1,20 @@
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 botId = requestBody.botId;
13
+ const messages = requestBody.messages;
14
+ const stream = streamText({
15
+ model: createBaseModel(botId),
16
+ messages: convertToModelMessages(messages),
17
+ includeRawChunks: true,
18
+ });
19
+ return stream.toUIMessageStreamResponse();
20
+ };
@@ -0,0 +1,10 @@
1
+ import { createCoze } from '@baishuyun/coze-provider';
2
+ import config from 'config';
3
+ export const createBaseModel = (botId) => {
4
+ const coze = createCoze({
5
+ apiKey: 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,10 @@
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 { logger } from "../../../logger/index.js";
6
+ import config from 'config';
7
+ import { determineUserIntentByInput } from "./utils.js";
5
8
  /**
6
9
  * 搭建表单
7
10
  * @param c
@@ -16,10 +19,16 @@ export const buildForm = async (c) => {
16
19
  catch (_) {
17
20
  return c.json({ error: "Invalid JSON" }, 400);
18
21
  }
19
- const isBuildStage = requestBody.stage === "build";
20
- const formName = requestBody.name || "未命名表单";
22
+ const intent = await determineUserIntentByInput(requestBody.text, requestBody.stage);
23
+ const isBuildStage = intent === "build";
24
+ const formName = requestBody.name;
25
+ const model = createModel([
26
+ () => createFieldsJsonTransformStream(isBuildStage),
27
+ () => new SuggestionTransformStream(isBuildStage),
28
+ ]);
29
+ logger.debug("intent: " + intent);
21
30
  const allMessages = [...requestBody.messages];
22
- if (isBuildStage) {
31
+ if (isBuildStage && formName) {
23
32
  allMessages.push({
24
33
  role: "user",
25
34
  parts: [
@@ -30,11 +39,12 @@ export const buildForm = async (c) => {
30
39
  ],
31
40
  });
32
41
  }
42
+ // clear empty text parts to avoid unnecessary streaming
43
+ allMessages.forEach((message) => {
44
+ message.parts = message.parts.filter((part) => part.type === "text" && part.text.trim() !== "");
45
+ });
33
46
  const stream = streamText({
34
- model: createModel([
35
- () => createFieldsJsonTransformStream(isBuildStage),
36
- () => new SuggestionTransformStream(isBuildStage),
37
- ]),
47
+ model,
38
48
  messages: convertToModelMessages(allMessages),
39
49
  includeRawChunks: true,
40
50
  headers: {
@@ -43,17 +53,5 @@ export const buildForm = async (c) => {
43
53
  "x-user-token": c.req.header("authorization") || "",
44
54
  },
45
55
  });
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
- });
56
+ return stream.toUIMessageStreamResponse();
59
57
  };
@@ -1,3 +1,4 @@
1
+ import { createFormBuildIntentAgent } from "@baishuyun/agents";
1
2
  import { createCoze } from "@baishuyun/coze-provider";
2
3
  import config from "config";
3
4
  export const createModel = (extraStreamTransformers) => {
@@ -9,3 +10,7 @@ export const createModel = (extraStreamTransformers) => {
9
10
  });
10
11
  return coze.chat("chat");
11
12
  };
13
+ export const createUserIntentAgent = () => {
14
+ const dsApiKey = config.get("agent.deepseekApiKey");
15
+ return createFormBuildIntentAgent(dsApiKey);
16
+ };
@@ -0,0 +1,30 @@
1
+ import { createUserIntentAgent } from "./model.js";
2
+ export const determineUserIntentByInput = async (input, userOriginIntent) => {
3
+ if (userOriginIntent === "design") {
4
+ return "design";
5
+ }
6
+ if (!input || input.trim() === "") {
7
+ return userOriginIntent;
8
+ }
9
+ const agent = createUserIntentAgent();
10
+ const intent = await agent.generate({
11
+ messages: [{
12
+ role: "user",
13
+ content: [{
14
+ type: "text",
15
+ text: input,
16
+ }]
17
+ }]
18
+ });
19
+ const result = intent.response.messages;
20
+ if (!result || result.length === 0) {
21
+ return userOriginIntent;
22
+ }
23
+ const lastMessage = result[result.length - 1];
24
+ if (!lastMessage.content || lastMessage.content.length === 0) {
25
+ return userOriginIntent;
26
+ }
27
+ const lastContent = lastMessage.content[0];
28
+ const intentText = lastContent.text === "build" ? "build" : lastContent.text === "design" ? "design" : userOriginIntent;
29
+ return intentText;
30
+ };
@@ -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);
@@ -36,19 +36,7 @@ export const queryReport = async (c) => {
36
36
  'x-user-id': Date.now().toString(), // uid,
37
37
  },
38
38
  });
39
- const response = stream.toUIMessageStreamResponse({
39
+ return stream.toUIMessageStreamResponse({
40
40
  originalMessages: messages, // 建议添加,便于消息 ID 管理
41
41
  });
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
42
  };
@@ -0,0 +1,7 @@
1
+ import { Hono } from 'hono';
2
+ import { connectToAgent } from '../../controllers/common/connect.controll.js';
3
+ export const createCommonRouter = () => {
4
+ const commonRouter = new Hono();
5
+ commonRouter.post('/connect', connectToAgent);
6
+ return commonRouter;
7
+ };
@@ -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) {
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.17",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,8 +22,9 @@
22
22
  "parse-sse": "^0.1.0",
23
23
  "pino": "^10.1.0",
24
24
  "zod": "^4.1.13",
25
- "@baishuyun/coze-provider": "0.0.16",
26
- "@baishuyun/types": "1.0.16"
25
+ "@baishuyun/coze-provider": "0.0.17",
26
+ "@baishuyun/agents": "0.0.17",
27
+ "@baishuyun/types": "1.0.17"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/config": "^3.3.5",
@@ -34,7 +35,7 @@
34
35
  "pm2": "^6.0.14",
35
36
  "tsx": "^4.7.1",
36
37
  "typescript": "^5.8.3",
37
- "@baishuyun/typescript-config": "0.0.16"
38
+ "@baishuyun/typescript-config": "0.0.17"
38
39
  },
39
40
  "scripts": {
40
41
  "dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
package/src/app/main.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  // 应用实例
6
6
  import app from '../config/hono.config.js';
7
+ import { createCommonRouter } from '../routes/common/common.route.js';
7
8
 
8
9
  // 子路由
9
10
  import { createFormRouter } from '../routes/form/form.route.js';
@@ -12,6 +13,7 @@ import { createReportRouter } from '../routes/report/report.route.js';
12
13
  // 挂载子路由
13
14
  app.route('web/api/form', createFormRouter());
14
15
  app.route('web/api/report', createReportRouter());
16
+ app.route('web/api/common', createCommonRouter());
15
17
 
16
18
  // 基础健康检查
17
19
  app.get('/web/api/health', (c) => {
@@ -0,0 +1,24 @@
1
+ import { convertToModelMessages, streamText } from 'ai';
2
+ import type { Context } from 'hono';
3
+ import { createBaseModel } from './model.js';
4
+
5
+ export const connectToAgent = async (c: Context) => {
6
+ let requestBody;
7
+ try {
8
+ const json = await c.req.json();
9
+ requestBody = json;
10
+ } catch (_) {
11
+ return c.json({ error: 'Invalid JSON' }, 400);
12
+ }
13
+
14
+ const botId = requestBody.botId;
15
+ const messages = requestBody.messages;
16
+
17
+ const stream = streamText({
18
+ model: createBaseModel(botId),
19
+ messages: convertToModelMessages(messages),
20
+ includeRawChunks: true,
21
+ });
22
+
23
+ return stream.toUIMessageStreamResponse();
24
+ };
@@ -0,0 +1,12 @@
1
+ import { createCoze } from '@baishuyun/coze-provider';
2
+ import config from 'config';
3
+
4
+ export const createBaseModel = (botId: string) => {
5
+ const coze = createCoze({
6
+ apiKey: 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,13 @@
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 { logger } from "../../../logger/index.js";
9
+ import config from 'config';
10
+ import { determineUserIntentByInput } from "./utils.js";
8
11
 
9
12
  /**
10
13
  * 搭建表单
@@ -21,11 +24,19 @@ export const buildForm = async (c: Context) => {
21
24
  return c.json({ error: "Invalid JSON" }, 400);
22
25
  }
23
26
 
24
- const isBuildStage = requestBody.stage === "build";
25
- const formName = requestBody.name || "未命名表单";
27
+ const intent = await determineUserIntentByInput(requestBody.text, requestBody.stage);
28
+
29
+ const isBuildStage = intent === "build";
30
+ const formName = requestBody.name;
31
+ const model = createModel([
32
+ () => createFieldsJsonTransformStream(isBuildStage),
33
+ () => new SuggestionTransformStream(isBuildStage),
34
+ ]);
35
+
36
+ logger.debug("intent: " + intent);
26
37
 
27
38
  const allMessages = [...requestBody.messages];
28
- if (isBuildStage) {
39
+ if (isBuildStage && formName) {
29
40
  allMessages.push({
30
41
  role: "user",
31
42
  parts: [
@@ -45,10 +56,7 @@ export const buildForm = async (c: Context) => {
45
56
  });
46
57
 
47
58
  const stream = streamText({
48
- model: createModel([
49
- () => createFieldsJsonTransformStream(isBuildStage),
50
- () => new SuggestionTransformStream(isBuildStage),
51
- ]),
59
+ model,
52
60
  messages: convertToModelMessages(allMessages),
53
61
  includeRawChunks: true,
54
62
  headers: {
@@ -1,4 +1,5 @@
1
1
  import type { LanguageModelV2StreamPart } from "@ai-sdk/provider";
2
+ import { createFormBuildIntentAgent } from "@baishuyun/agents";
2
3
  import { createCoze } from "@baishuyun/coze-provider";
3
4
  import config from "config";
4
5
 
@@ -14,3 +15,8 @@ export const createModel = (
14
15
 
15
16
  return coze.chat("chat");
16
17
  };
18
+
19
+ export const createUserIntentAgent = () => {
20
+ const dsApiKey = config.get<string>("agent.deepseekApiKey");
21
+ return createFormBuildIntentAgent(dsApiKey);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { createUserIntentAgent } from "./model.js"
2
+
3
+ export const determineUserIntentByInput = async (input: string, userOriginIntent: "build" | "design" | null): Promise<"build" | "design" | null> => {
4
+ if (userOriginIntent === "design") {
5
+ return "design";
6
+ }
7
+
8
+ if (!input || input.trim() === "") {
9
+ return userOriginIntent;
10
+ }
11
+
12
+ const agent = createUserIntentAgent();
13
+
14
+ const intent = await agent.generate({
15
+ messages: [{
16
+ role: "user",
17
+ content: [{
18
+ type: "text",
19
+ text: input,
20
+ }]
21
+ }]
22
+ })
23
+
24
+ const result = intent.response.messages;
25
+ if (!result || result.length === 0) {
26
+ return userOriginIntent;
27
+ }
28
+
29
+ const lastMessage = result[result.length - 1];
30
+
31
+ if (!lastMessage.content || lastMessage.content.length === 0) {
32
+ return userOriginIntent;
33
+ }
34
+
35
+ const lastContent = lastMessage.content[0] as {
36
+ type: "text";
37
+ text: string;
38
+ };
39
+
40
+ const intentText = lastContent.text === "build" ? "build" : lastContent.text === "design" ? "design" : userOriginIntent;
41
+
42
+ return intentText;
43
+ }
@@ -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
  }
@@ -0,0 +1,10 @@
1
+ import { Hono } from 'hono';
2
+ import { connectToAgent } from '../../controllers/common/connect.controll.js';
3
+
4
+ export const createCommonRouter = () => {
5
+ const commonRouter = new Hono();
6
+
7
+ commonRouter.post('/connect', connectToAgent);
8
+
9
+ return commonRouter;
10
+ };