plugin-custom-llm 1.2.0 → 1.2.1

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.
@@ -0,0 +1,8 @@
1
+ import { Plugin } from '@nocobase/client';
2
+ export declare class PluginCustomLLMClient extends Plugin {
3
+ afterAdd(): Promise<void>;
4
+ beforeLoad(): Promise<void>;
5
+ load(): Promise<void>;
6
+ private get aiPlugin();
7
+ }
8
+ export default PluginCustomLLMClient;
@@ -1,10 +1 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):"function"==typeof define&&define.amd?define("plugin-custom-llm",["react","@nocobase/plugin-ai/client","@nocobase/client","@nocobase/utils/client","antd","react-i18next"],t):"object"==typeof exports?exports["plugin-custom-llm"]=t(require("react"),require("@nocobase/plugin-ai/client"),require("@nocobase/client"),require("@nocobase/utils/client"),require("antd"),require("react-i18next")):e["plugin-custom-llm"]=t(e.react,e["@nocobase/plugin-ai/client"],e["@nocobase/client"],e["@nocobase/utils/client"],e.antd,e["react-i18next"])}(self,function(e,t,n,o,r,i){return function(){"use strict";var a={772:function(e){e.exports=n},645:function(e){e.exports=t},584:function(e){e.exports=o},721:function(e){e.exports=r},156:function(t){t.exports=e},238:function(e){e.exports=i}},c={};function u(e){var t=c[e];if(void 0!==t)return t.exports;var n=c[e]={exports:{}};return a[e](n,n.exports,u),n.exports}u.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return u.d(t,{a:t}),t},u.d=function(e,t){for(var n in t)u.o(t,n)&&!u.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},u.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},u.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var l={};return!function(){u.r(l),u.d(l,{PluginCustomLLMClient:function(){return g},default:function(){return S}});var e=u(772),t=u(156),n=u.n(t),o=u(584),r=u(238),i="@nocobase/plugin-custom-llm",a=u(721),c=u(645),p=function(){var t=(0,r.useTranslation)(i,{nsMode:"fallback"}).t;return n().createElement("div",{style:{marginBottom:24}},n().createElement(a.Collapse,{bordered:!1,size:"small",items:[{key:"options",label:t("Options"),forceRender:!0,children:n().createElement(e.SchemaComponent,{schema:{type:"void",name:"custom-llm",properties:{temperature:{title:(0,o.tval)("Temperature",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:.7,"x-component-props":{step:.1,min:0,max:2}},maxCompletionTokens:{title:(0,o.tval)("Max completion tokens",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:-1},topP:{title:(0,o.tval)("Top P",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1,"x-component-props":{step:.1,min:0,max:1}},frequencyPenalty:{title:(0,o.tval)("Frequency penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},presencePenalty:{title:(0,o.tval)("Presence penalty",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:0,"x-component-props":{step:.1,min:-2,max:2}},responseFormat:{title:(0,o.tval)("Response format",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Select",enum:[{label:t("Text"),value:"text"},{label:t("JSON"),value:"json_object"}],default:"text"},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:6e4},maxRetries:{title:(0,o.tval)("Max retries",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber",default:1}}}})}]}))},s={components:{ProviderSettingsForm:function(){return n().createElement(e.SchemaComponent,{schema:{type:"void",properties:{apiKey:{title:(0,o.tval)("API Key",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"TextAreaWithGlobalScope"},disableStream:{title:(0,o.tval)("Disable streaming",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Disable streaming description",{ns:i})},streamKeepAlive:{title:(0,o.tval)("Stream keepalive",{ns:i}),type:"boolean","x-decorator":"FormItem","x-component":"Checkbox","x-content":(0,o.tval)("Stream keepalive description",{ns:i})},keepAliveIntervalMs:{title:(0,o.tval)("Keepalive interval (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"5000",min:1e3,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Keepalive interval description",{ns:i})},keepAliveContent:{title:(0,o.tval)("Keepalive content",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input","x-component-props":{placeholder:"..."},description:(0,o.tval)("Keepalive content description",{ns:i})},timeout:{title:(0,o.tval)("Timeout (ms)",{ns:i}),type:"number","x-decorator":"FormItem","x-component":"InputNumber","x-component-props":{placeholder:"120000",min:0,step:1e3,style:{width:"100%"}},description:(0,o.tval)("Timeout description",{ns:i})},requestConfig:{title:(0,o.tval)("Request config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({extraHeaders:{},extraBody:{},modelKwargs:{}},null,2),rows:6,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Request config description",{ns:i})},responseConfig:{title:(0,o.tval)("Response config (JSON)",{ns:i}),type:"string","x-decorator":"FormItem","x-component":"Input.TextArea","x-component-props":{placeholder:JSON.stringify({contentPath:"auto",reasoningKey:"reasoning_content",responseMapping:{content:"message.response"}},null,2),rows:8,style:{fontFamily:"monospace",fontSize:12}},description:(0,o.tval)("Response config description",{ns:i})}}}})},ModelSettingsForm:function(){return n().createElement(e.SchemaComponent,{components:{Options:p,ModelSelect:c.ModelSelect},schema:{type:"void",properties:{model:{title:(0,o.tval)("Model",{ns:i}),type:"string",required:!0,"x-decorator":"FormItem","x-component":"ModelSelect"},options:{type:"void","x-component":"Options"}}}})}}};function m(e,t,n,o,r,i,a){try{var c=e[i](a),u=c.value}catch(e){n(e);return}c.done?t(u):Promise.resolve(u).then(o,r)}function f(e){return function(){var t=this,n=arguments;return new Promise(function(o,r){var i=e.apply(t,n);function a(e){m(i,o,r,a,c,"next",e)}function c(e){m(i,o,r,a,c,"throw",e)}a(void 0)})}}function d(e,t,n){return(d=x()?Reflect.construct:function(e,t,n){var o=[null];o.push.apply(o,t);var r=new(Function.bind.apply(e,o));return n&&b(r,n.prototype),r}).apply(null,arguments)}function y(e){return(y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function b(e,t){return(b=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function v(e){var t="function"==typeof Map?new Map:void 0;return(v=function(e){if(null===e||-1===Function.toString.call(e).indexOf("[native code]"))return e;if("function"!=typeof e)throw TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,n)}function n(){return d(e,arguments,y(this).constructor)}return n.prototype=Object.create(e.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),b(n,e)})(e)}function x(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(x=function(){return!!e})()}function h(e,t){var n,o,r,i,a={label:0,sent:function(){if(1&r[0])throw r[1];return r[1]},trys:[],ops:[]};return i={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function c(i){return function(c){var u=[i,c];if(n)throw TypeError("Generator is already executing.");for(;a;)try{if(n=1,o&&(r=2&u[0]?o.return:u[0]?o.throw||((r=o.return)&&r.call(o),0):o.next)&&!(r=r.call(o,u[1])).done)return r;switch(o=0,r&&(u=[2&u[0],r.value]),u[0]){case 0:case 1:r=u;break;case 4:return a.label++,{value:u[1],done:!1};case 5:a.label++,o=u[1],u=[0];continue;case 7:u=a.ops.pop(),a.trys.pop();continue;default:if(!(r=(r=a.trys).length>0&&r[r.length-1])&&(6===u[0]||2===u[0])){a=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]<r[3])){a.label=u[1];break}if(6===u[0]&&a.label<r[1]){a.label=r[1],r=u;break}if(r&&a.label<r[2]){a.label=r[2],a.ops.push(u);break}r[2]&&a.ops.pop(),a.trys.pop();continue}u=t.call(e,a)}catch(e){u=[6,e],o=0}finally{n=r=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}}}var g=function(e){var t;if("function"!=typeof e&&null!==e)throw TypeError("Super expression must either be null or a function");function n(){var e,t;if(!(this instanceof n))throw TypeError("Cannot call a class as a function");return e=n,t=arguments,e=y(e),function(e,t){var n;if(t&&("object"==((n=t)&&"undefined"!=typeof Symbol&&n.constructor===Symbol?"symbol":typeof n)||"function"==typeof t))return t;if(void 0===e)throw ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(this,x()?Reflect.construct(e,t||[],y(this).constructor):e.apply(this,t))}return n.prototype=Object.create(e&&e.prototype,{constructor:{value:n,writable:!0,configurable:!0}}),e&&b(n,e),t=[{key:"afterAdd",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"beforeLoad",value:function(){return f(function(){return h(this,function(e){return[2]})})()}},{key:"load",value:function(){var e=this;return f(function(){return h(this,function(t){return e.aiPlugin.aiManager.registerLLMProvider("custom-llm",s),[2]})})()}},{key:"aiPlugin",get:function(){return this.app.pm.get("ai")}}],function(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}(n.prototype,t),n}(v(e.Plugin)),S=g}(),l}()});
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const ModelSettingsForm: React.FC;
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const ProviderSettingsForm: React.FC;
@@ -0,0 +1,2 @@
1
+ import { LLMProviderOptions } from '@nocobase/plugin-ai/client';
2
+ export declare const customLLMProviderOptions: LLMProviderOptions;
@@ -0,0 +1,2 @@
1
+ export declare const namespace = "@nocobase/plugin-custom-llm";
2
+ export declare function useT(): import("react-i18next").TFunction<"@nocobase/plugin-custom-llm", undefined>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ declare const _default: Record<string, ModelConstructor>;
10
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { Plugin } from '@nocobase/client';
2
+ export declare class PluginCustomLlmClient extends Plugin {
3
+ load(): Promise<void>;
4
+ }
5
+ export default PluginCustomLlmClient;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  module.exports = {
11
2
  "@nocobase/client": "2.0.20",
12
3
  "@nocobase/plugin-ai": "2.0.20",
@@ -14,7 +5,9 @@ module.exports = {
14
5
  "@nocobase/server": "2.0.20",
15
6
  "@nocobase/flow-engine": "2.0.20",
16
7
  "@nocobase/database": "2.0.20",
17
- "react": "18.2.0",
8
+ "axios": "1.14.0",
9
+ "@nocobase/actions": "2.0.20",
10
+ "react": "18.3.1",
18
11
  "@nocobase/utils": "2.0.20",
19
12
  "antd": "5.24.2"
20
13
  };
@@ -0,0 +1,2 @@
1
+ export * from './server';
2
+ export { default } from './server';
package/dist/index.js CHANGED
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -0,0 +1 @@
1
+ export { default } from './plugin';
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -0,0 +1,54 @@
1
+ import { LLMProvider, LLMProviderMeta } from '@nocobase/plugin-ai';
2
+ import { Model } from '@nocobase/database';
3
+ import { Context } from '@nocobase/actions';
4
+ import type { ParsedAttachmentResult } from '@nocobase/plugin-ai';
5
+ export declare class CustomLLMProvider extends LLMProvider {
6
+ get baseURL(): any;
7
+ private get requestConfig();
8
+ private get responseConfig();
9
+ createModel(): any;
10
+ parseResponseChunk(chunk: any): string | null;
11
+ parseResponseMessage(message: Model): {
12
+ key: Model;
13
+ content: Record<string, any>;
14
+ role: Model;
15
+ };
16
+ parseReasoningContent(chunk: any): {
17
+ status: string;
18
+ content: string;
19
+ } | null;
20
+ /**
21
+ * Extract response metadata from LLM output for post-save enrichment.
22
+ * Sanitizes overly long message IDs from Gemini or other providers.
23
+ */
24
+ parseResponseMetadata(output: any): any;
25
+ parseResponseError(err: any): any;
26
+ /**
27
+ * Self-contained file reading that correctly handles the APP_PUBLIC_PATH prefix.
28
+ *
29
+ * plugin-ai's encodeLocalFile does path.join(cwd, url) without stripping
30
+ * APP_PUBLIC_PATH, so when the app is deployed under a sub-path (e.g. /my-app)
31
+ * the resolved path becomes '{cwd}/my-app/storage/uploads/…' which does not exist.
32
+ * We cannot fix that in plugin-ai (core), so we re-implement file reading here
33
+ * with the prefix stripped before the cwd join.
34
+ */
35
+ private readFileAsBase64;
36
+ /**
37
+ * Override parseAttachment to convert all attachments into formats that
38
+ * generic OpenAI-compatible endpoints actually support:
39
+ *
40
+ * - Images → image_url block with base64 data URI (vision models)
41
+ * - Text files → text block with decoded UTF-8 content
42
+ * - Binary → text block with base64 data URI (multi-modal or fallback)
43
+ *
44
+ * The base-class implementation returns a LangChain ContentBlock.Multimodal.File
45
+ * (`type: 'file'`) for non-image attachments. LangChain serialises this as the
46
+ * newer OpenAI Files API format which most custom/local endpoints do NOT understand,
47
+ * causing file content to be silently dropped.
48
+ *
49
+ * This method is entirely self-contained — it does not call super — so it is
50
+ * safe to use without modifying plugin-ai core.
51
+ */
52
+ parseAttachment(ctx: Context, attachment: any): Promise<ParsedAttachmentResult>;
53
+ }
54
+ export declare const customLLMProviderOptions: LLMProviderMeta;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __create = Object.create;
11
2
  var __defProp = Object.defineProperty;
12
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -42,6 +33,8 @@ __export(custom_llm_exports, {
42
33
  module.exports = __toCommonJS(custom_llm_exports);
43
34
  var import_plugin_ai = require("@nocobase/plugin-ai");
44
35
  var import_node_path = __toESM(require("node:path"));
36
+ var import_promises = __toESM(require("node:fs/promises"));
37
+ var import_axios = __toESM(require("axios"));
45
38
  const KEEPALIVE_PREFIX = "\u200B\u200B\u200B";
46
39
  function requireFromApp(moduleName) {
47
40
  const appNodeModules = process.env.NODE_MODULES_PATH || import_node_path.default.join(process.cwd(), "node_modules");
@@ -100,11 +93,41 @@ function extractTextContent(content, contentPath) {
100
93
  }
101
94
  return "";
102
95
  }
103
- function safeParseJSON(str) {
96
+ function isTextMimetype(mimetype) {
97
+ if (!mimetype) return false;
98
+ if (mimetype.startsWith("text/")) return true;
99
+ const TEXT_APPLICATION_TYPES = /* @__PURE__ */ new Set([
100
+ "application/json",
101
+ "application/xml",
102
+ "application/xhtml+xml",
103
+ "application/atom+xml",
104
+ "application/rss+xml",
105
+ "application/csv",
106
+ "application/javascript",
107
+ "application/typescript",
108
+ "application/x-javascript",
109
+ "application/x-typescript",
110
+ "application/x-yaml",
111
+ "application/yaml",
112
+ "application/x-json",
113
+ "application/geo+json",
114
+ "application/ld+json",
115
+ "application/manifest+json",
116
+ "application/graphql",
117
+ "application/x-www-form-urlencoded",
118
+ "application/toml",
119
+ "application/x-sh",
120
+ "application/x-shellscript",
121
+ "application/sql"
122
+ ]);
123
+ return TEXT_APPLICATION_TYPES.has(mimetype);
124
+ }
125
+ function safeParseJSON(str, fieldName) {
104
126
  if (!str || typeof str !== "string") return {};
105
127
  try {
106
128
  return JSON.parse(str);
107
- } catch {
129
+ } catch (e) {
130
+ console.warn(`[CustomLLM] Failed to parse ${fieldName || "JSON config"}: ${e.message}`);
108
131
  return {};
109
132
  }
110
133
  }
@@ -170,13 +193,13 @@ function createMappingFetch(responseMapping) {
170
193
 
171
194
  `));
172
195
  } else {
173
- controller.enqueue(encoder.encode(line + "\n"));
196
+ controller.enqueue(encoder.encode(line + "\n\n"));
174
197
  }
175
198
  } catch {
176
- controller.enqueue(encoder.encode(line + "\n"));
199
+ controller.enqueue(encoder.encode(line + "\n\n"));
177
200
  }
178
201
  } else {
179
- controller.enqueue(encoder.encode(line + "\n"));
202
+ controller.enqueue(encoder.encode(line + "\n\n"));
180
203
  }
181
204
  }
182
205
  }
@@ -391,11 +414,11 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
391
414
  }
392
415
  get requestConfig() {
393
416
  var _a;
394
- return safeParseJSON((_a = this.serviceOptions) == null ? void 0 : _a.requestConfig);
417
+ return safeParseJSON((_a = this.serviceOptions) == null ? void 0 : _a.requestConfig, "requestConfig");
395
418
  }
396
419
  get responseConfig() {
397
420
  var _a;
398
- return safeParseJSON((_a = this.serviceOptions) == null ? void 0 : _a.responseConfig);
421
+ return safeParseJSON((_a = this.serviceOptions) == null ? void 0 : _a.responseConfig, "responseConfig");
399
422
  }
400
423
  createModel() {
401
424
  var _a;
@@ -528,6 +551,88 @@ class CustomLLMProvider extends import_plugin_ai.LLMProvider {
528
551
  parseResponseError(err) {
529
552
  return (err == null ? void 0 : err.message) ?? "Unexpected LLM service error";
530
553
  }
554
+ /**
555
+ * Self-contained file reading that correctly handles the APP_PUBLIC_PATH prefix.
556
+ *
557
+ * plugin-ai's encodeLocalFile does path.join(cwd, url) without stripping
558
+ * APP_PUBLIC_PATH, so when the app is deployed under a sub-path (e.g. /my-app)
559
+ * the resolved path becomes '{cwd}/my-app/storage/uploads/…' which does not exist.
560
+ * We cannot fix that in plugin-ai (core), so we re-implement file reading here
561
+ * with the prefix stripped before the cwd join.
562
+ */
563
+ async readFileAsBase64(ctx, attachment) {
564
+ const fileManager = this.app.pm.get("file-manager");
565
+ const rawUrl = await fileManager.getFileURL(attachment);
566
+ const url = decodeURIComponent(rawUrl);
567
+ if (url.startsWith("http://") || url.startsWith("https://")) {
568
+ const referer = ctx.get("referer") || "";
569
+ const ua = ctx.get("user-agent") || "";
570
+ const response = await import_axios.default.get(url, {
571
+ responseType: "arraybuffer",
572
+ // Default 30s timeout to prevent slow file servers from blocking indefinitely
573
+ timeout: 3e4,
574
+ headers: { referer, "User-Agent": ua }
575
+ });
576
+ return Buffer.from(response.data).toString("base64");
577
+ }
578
+ let localPath = url;
579
+ const appPublicPath = (process.env.APP_PUBLIC_PATH || "/").replace(/\/+$/, "");
580
+ if (appPublicPath && localPath.startsWith(appPublicPath + "/")) {
581
+ localPath = localPath.slice(appPublicPath.length);
582
+ }
583
+ const storageRoot = import_node_path.default.resolve(process.cwd());
584
+ const absPath = import_node_path.default.resolve(storageRoot, localPath.replace(/^\//, ""));
585
+ if (!absPath.startsWith(storageRoot + import_node_path.default.sep) && absPath !== storageRoot) {
586
+ throw new Error(`Attachment path escapes storage root: ${localPath}`);
587
+ }
588
+ const data = await import_promises.default.readFile(absPath);
589
+ return Buffer.from(data).toString("base64");
590
+ }
591
+ /**
592
+ * Override parseAttachment to convert all attachments into formats that
593
+ * generic OpenAI-compatible endpoints actually support:
594
+ *
595
+ * - Images → image_url block with base64 data URI (vision models)
596
+ * - Text files → text block with decoded UTF-8 content
597
+ * - Binary → text block with base64 data URI (multi-modal or fallback)
598
+ *
599
+ * The base-class implementation returns a LangChain ContentBlock.Multimodal.File
600
+ * (`type: 'file'`) for non-image attachments. LangChain serialises this as the
601
+ * newer OpenAI Files API format which most custom/local endpoints do NOT understand,
602
+ * causing file content to be silently dropped.
603
+ *
604
+ * This method is entirely self-contained — it does not call super — so it is
605
+ * safe to use without modifying plugin-ai core.
606
+ */
607
+ async parseAttachment(ctx, attachment) {
608
+ const mimetype = attachment.mimetype || "application/octet-stream";
609
+ const filename = attachment.filename || attachment.name || "file";
610
+ const data = await this.readFileAsBase64(ctx, attachment);
611
+ if (mimetype.startsWith("image/")) {
612
+ return {
613
+ placement: "contentBlocks",
614
+ content: {
615
+ type: "image_url",
616
+ image_url: { url: `data:${mimetype};base64,${data}` }
617
+ }
618
+ };
619
+ }
620
+ let textContent;
621
+ if (isTextMimetype(mimetype)) {
622
+ const decoded = Buffer.from(data, "base64").toString("utf-8");
623
+ textContent = `<attachment filename="${filename}" type="${mimetype}">
624
+ ${decoded}
625
+ </attachment>`;
626
+ } else {
627
+ textContent = `<attachment filename="${filename}" type="${mimetype}">
628
+ data:${mimetype};base64,${data}
629
+ </attachment>`;
630
+ }
631
+ return {
632
+ placement: "contentBlocks",
633
+ content: { type: "text", text: textContent }
634
+ };
635
+ }
531
636
  }
532
637
  const customLLMProviderOptions = {
533
638
  title: "Custom LLM (OpenAI Compatible)",
@@ -0,0 +1,12 @@
1
+ import { Plugin } from '@nocobase/server';
2
+ export declare class PluginCustomLLMServer extends Plugin {
3
+ afterAdd(): Promise<void>;
4
+ beforeLoad(): Promise<void>;
5
+ load(): Promise<void>;
6
+ install(): Promise<void>;
7
+ afterEnable(): Promise<void>;
8
+ afterDisable(): Promise<void>;
9
+ remove(): Promise<void>;
10
+ private get aiPlugin();
11
+ }
12
+ export default PluginCustomLLMServer;
@@ -1,12 +1,3 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
1
  var __defProp = Object.defineProperty;
11
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "AI LLM: Custom (OpenAI Compatible)",
4
4
  "displayName.zh-CN": "AI LLM:自定义(OpenAI 兼容)",
5
5
  "description": "OpenAI-compatible LLM provider with auto response format detection for external LLM services.",
6
- "version": "1.2.0",
6
+ "version": "1.2.1",
7
7
  "main": "dist/server/index.js",
8
8
  "nocobase": {
9
9
  "supportedVersions": [