Quellcode durchsuchen

💊 佐证资料上传AI生成说明

快乐的梦鱼 vor 1 Monat
Ursprung
Commit
1e38ffd82b

+ 22 - 0
package-lock.json

@@ -27,6 +27,7 @@
         "mitt": "^3.0.1",
         "nprogress": "^0.2.0",
         "nuxt": "^3.17.6",
+        "openai": "^6.38.0",
         "pinia": "^3.0.3",
         "quill-image-uploader": "^1.3.0",
         "tinymce": "^8.1.2",
@@ -10965,6 +10966,27 @@
         "node": ">=8"
       }
     },
+    "node_modules/openai": {
+      "version": "6.38.0",
+      "resolved": "https://registry.npmmirror.com/openai/-/openai-6.38.0.tgz",
+      "integrity": "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==",
+      "license": "Apache-2.0",
+      "bin": {
+        "openai": "bin/cli"
+      },
+      "peerDependencies": {
+        "ws": "^8.18.0",
+        "zod": "^3.25 || ^4.0"
+      },
+      "peerDependenciesMeta": {
+        "ws": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/os-name": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/os-name/-/os-name-1.0.3.tgz",

+ 1 - 0
package.json

@@ -34,6 +34,7 @@
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",
     "nuxt": "^3.17.6",
+    "openai": "^6.38.0",
     "pinia": "^3.0.3",
     "quill-image-uploader": "^1.3.0",
     "tinymce": "^8.1.2",

+ 84 - 1
src/api/RequestModules.ts

@@ -15,7 +15,8 @@ import {
   defaultResponseDataGetErrorInfo, defaultResponseDataHandlerCatch, 
   RequestResponse,
   WebFetchImplementer,
-  appendGetUrlParams, appendPostParams
+  appendGetUrlParams, appendPostParams,
+  RandomUtils
 } from "@imengyu/imengyu-utils";
 import type { DataModel, KeyValue, NewDataModel } from "@imengyu/js-request-transform";
 import { useAuthStore } from "@/stores/auth";
@@ -234,3 +235,85 @@ export class AppServerRequestModule<T extends DataModel> extends RequestCoreInst
     this.config.reportError = reportError;
   }
 }
+/**
+ * 更新服务请求模块
+ */
+export class MengyuServerRequestModule<T extends DataModel> extends RequestCoreInstance<T> {
+  constructor() {
+    super(WebFetchImplementer);
+    this.config.baseUrl = 'https://update-server1.imengyu.top';
+    this.config.requestInceptor = (url, req) => {
+      if (!req.header)
+        req.header = {};
+      req.header['Authorization'] = JSON.stringify({
+        "apiKey":"MQQDGbn8QfFJ7kStNtkxwifHP4sBTSDd",
+        "apiSecret":"3BNAdR7NXGwfiRmQZkRcRM8PsyHPeBmaay2k2F4TXhGEziXSJ3ceEtH2ApfHsMhR"
+      });
+      url = appendGetUrlParams(url, 'identifier', this.getDeviceUid());
+      return { newUrl: url, newReq: req };
+    };
+    this.config.responseDataHandler = async function responseDataHandler<T extends DataModel>(response: RequestResponse, req: RequestOptions, resultModelClass: NewDataModel | undefined, instance: RequestCoreInstance<T>): Promise<RequestApiResult<T>> {
+      const method = req.method || 'GET';
+      try {
+        const json = await response.json();
+        if (response.ok) {
+          if (!json) {
+            throw new RequestApiError(
+              'businessError',
+              '后端未返回数据',
+              '',
+              response.status,
+              null,
+              null,
+              response.headers
+            );
+          }
+          if (!json.success)
+            throw new RequestApiError(
+              'businessError',
+              json.message,
+              json.code.toString(),
+              json.code,
+              json,
+              json,
+              response.headers,
+            );
+          
+          return new RequestApiResult(
+            resultModelClass ?? instance.config.modelClassCreator,
+            json?.code || response.status,
+            json.message,
+            json.data,
+            json
+          );
+        }
+        else {
+          throw json;
+        }
+
+      } catch (err) {
+        if (err instanceof RequestApiError) {
+          throw response;
+        }
+        //错误统一处理
+        return new Promise<RequestApiResult<T>>((resolve, reject) => {
+          defaultResponseDataHandlerCatch(method, req, response, null, err as any, '', response.url, reject, instance);
+        });
+      }
+    };
+  }
+
+  private static readonly DEVICE_UID_KEY = 'deviceUid';
+  private uid = '';
+
+  getDeviceUid() {
+    if (!this.uid) {
+      this.uid = localStorage.getItem(MengyuServerRequestModule.DEVICE_UID_KEY) ?? '';
+      if (!this.uid) {
+        this.uid = RandomUtils.genNonDuplicateID(20);
+        localStorage.setItem(MengyuServerRequestModule.DEVICE_UID_KEY, this.uid);
+      }
+    }
+    return this.uid;
+  }
+}

+ 21 - 0
src/api/agent/Agent.ts

@@ -0,0 +1,21 @@
+import { DataModel } from "@imengyu/js-request-transform";
+import type OpenAI from "openai";
+import { MengyuServerRequestModule } from "../RequestModules";
+
+export class AgentApi extends MengyuServerRequestModule<DataModel> {
+
+  /**
+   * 非流式对话
+   * 后端接口:POST /content/agent/chat,body: { options }
+   */
+  chat(options: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming) {
+    return this.post<OpenAI.Chat.Completions.ChatCompletion>(
+      '/content/agent/chat',
+      { options },
+      'AI对话(非流式)',
+    );
+  }
+}
+
+export default new AgentApi();
+

+ 66 - 0
src/api/agent/AgentWorks.ts

@@ -0,0 +1,66 @@
+import { DataModel } from "@imengyu/js-request-transform";
+import AgentApi from "./Agent";
+import { MengyuServerRequestModule } from "../RequestModules";
+
+export class AgentWorkApi extends MengyuServerRequestModule<DataModel> {
+  constructor() {
+    super();
+  }
+
+  private static readonly WORK_MINI_MODEL_NAME = 'hunyuan-2.0-instruct-20251111';
+
+  private extractJsonObject<T>(content: string, fallback: T): T {
+    try {
+      return JSON.parse(content) as T;
+    } catch {
+      const start = content.indexOf('{');
+      const end = content.lastIndexOf('}');
+      if (start >= 0 && end > start) {
+        try {
+          return JSON.parse(content.slice(start, end + 1)) as T;
+        } catch {
+          return fallback;
+        }
+      }
+      return fallback;
+    }
+  }
+
+  /**
+   * 通过用户输入的生平事迹生成文件描述
+   */
+  async generateFileDescByInputDoc(inputDoc: string, fileName: string): Promise<string> {
+    try {
+      const result = await AgentApi.chat({
+        model: AgentWorkApi.WORK_MINI_MODEL_NAME,
+        messages: [
+          {
+            role: "system",
+            content: `你是文件描述生成器。你负责根据用户输入的工作日志事迹,用户本次上传文件的文件名称,
+            生成文件描述。例如:用户输入的工作日志事迹为:张三,2026年1月1日,工作内容为:写代码,工作结果为:完成一个功能。
+            用户本次上传文件的文件名称为:代码.txt,则文件描述为:张三在2026年1月1日写代码,完成一个功能。
+            要求:
+            1) 文件描述要简洁明了,不要超过200字。
+            2) 文件描述要符合用户输入的工作日志事迹,不要偏离主题。
+            3) 文件描述要符合用户本次上传文件的文件名称,不要偏离主题。
+            4) 返回 JSON:{ "desc": "文本" }。不要返回其他内容。`,
+          },
+          {
+            role: "user",
+            content: `用户输入的工作日志事迹:\n${inputDoc}
+            用户本次上传文件的文件名称:${fileName}`,
+          },
+        ],
+      });
+      const content = (result.data! as any).choices[0].message.content || "{}";
+      const parsed = this.extractJsonObject<{ desc?: string }>(content, {});
+      return (parsed.desc || "").trim();
+    } catch {
+      return "";
+    }
+  }
+
+}
+
+export default new AgentWorkApi();
+

+ 23 - 1
src/pages/collect/assessment/components/EvaluationFormBlock.vue

@@ -65,7 +65,7 @@
           {{ child.name }} ({{ child.points }}分)
         </a-checkbox>
       </template>
-      <div class="annex-upload-block">
+      <div class="annex-upload-block border border-gray-200 rounded-md p-2 mt-3">
         <a-typography-text type="secondary">{{ readonly ? '佐证资料' : '佐证材料上传' }}</a-typography-text>
         <a-alert
           v-if="!currentForm.id"
@@ -134,6 +134,7 @@ import {
   type CheckItemInfo,
   type SelfAssessmentDetail,
 } from '@/api/collect/AssessmentContent';
+import AgentWorkApi from '@/api/agent/AgentWorks';
 import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
 import { useAliOssUploadCo } from '@/common/upload/AliOssUploadCo';
 import type { RadioValueFormItemProps, SelectIdProps } from '@imengyu/vue-dynamic-form-ant';
@@ -393,10 +394,26 @@ function setAnnexFileList(itemId: number, list: UploadProps['fileList']) {
   annexFileMap.value[itemId] = list ?? [];
 }
 
+function buildAnnexDescInputDoc() {
+  const content = props.currentForm.content;
+  if (!content)
+    return '';
+  const lines: string[] = [];
+  for (let i = 0; i <= 6; i++) {
+    const value = String(content[`item${i}`] ?? '').trim();
+    if (!value)
+      continue;
+    lines.push(`${contentItemLabels[i]} ${value}`);
+  }
+  return lines.join('\n');
+}
+
 function formatAnnexDisplayName(desc: string | null | undefined, name: string) {
   const descText = (desc ?? '').trim();
   if (!descText)
     return name;
+  if (descText.length > 60)
+    return `${descText.slice(0, 60)}...(${name})`;
   return `${descText}(${name})`;
 }
 
@@ -521,10 +538,15 @@ async function annexCustomRequest(itemId: number, options: Parameters<NonNullabl
         onError: (err) => reject(err),
       });
     });
+    const inputDoc = buildAnnexDescInputDoc();
+    const desc = inputDoc
+      ? await AgentWorkApi.generateFileDescByInputDoc(inputDoc, raw.name)
+      : '';
     await AssessmentContentApi.saveAnnex({
       name: raw.name,
       formId,
       itemId,
+      desc,
       url: uploadedUrl,
       type: getCheckAnnexType(mimetype),
       mimetype,