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.
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.js +0 -9
- package/dist/client/llm-providers/custom-llm/ModelSettings.d.ts +2 -0
- package/dist/client/llm-providers/custom-llm/ProviderSettings.d.ts +2 -0
- package/dist/client/llm-providers/custom-llm/index.d.ts +2 -0
- package/dist/client/locale.d.ts +2 -0
- package/dist/client/models/index.d.ts +10 -0
- package/dist/client/plugin.d.ts +5 -0
- package/dist/externalVersion.js +3 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +0 -9
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +0 -9
- package/dist/server/llm-providers/custom-llm.d.ts +54 -0
- package/dist/server/llm-providers/custom-llm.js +121 -16
- package/dist/server/plugin.d.ts +12 -0
- package/dist/server/plugin.js +0 -9
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -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,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;
|
package/dist/externalVersion.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
|
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
|
-
"
|
|
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
|
};
|
package/dist/index.d.ts
ADDED
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';
|
package/dist/server/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,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
|
|
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;
|
package/dist/server/plugin.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 __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.
|
|
6
|
+
"version": "1.2.1",
|
|
7
7
|
"main": "dist/server/index.js",
|
|
8
8
|
"nocobase": {
|
|
9
9
|
"supportedVersions": [
|