079project 3.0.0 → 5.0.0
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/GroupStarter.cjs +396 -30
- package/forwarder.js +312 -55
- package/main_Serve.cjs +583 -105
- package/main_Study.cjs +581 -68
- package/notes.txt +241 -0
- package/package.json +7 -1
- package/note.txt +0 -5
- package/notebook.txt +0 -8
package/forwarder.js
CHANGED
|
@@ -285,6 +285,43 @@ app.get('/', (req, res) => {
|
|
|
285
285
|
<button type="submit" class="btn btn-primary">应用参数</button>
|
|
286
286
|
<button type="button" id="resetParams" class="btn btn-secondary">重置默认值</button>
|
|
287
287
|
</div>
|
|
288
|
+
<hr class="my-4"/>
|
|
289
|
+
<h6 class="mb-3">激活与传递函数</h6>
|
|
290
|
+
<div class="row g-3">
|
|
291
|
+
<div class="col-md-6">
|
|
292
|
+
<label for="activationType" class="form-label">Activation Function</label>
|
|
293
|
+
<select id="activationType" class="form-select">
|
|
294
|
+
<option value="identity">identity</option>
|
|
295
|
+
<option value="relu" selected>relu</option>
|
|
296
|
+
<option value="leaky_relu">leaky_relu</option>
|
|
297
|
+
<option value="tanh">tanh</option>
|
|
298
|
+
<option value="sigmoid">sigmoid</option>
|
|
299
|
+
<option value="elu">elu</option>
|
|
300
|
+
<option value="softplus">softplus</option>
|
|
301
|
+
<option value="gelu">gelu</option>
|
|
302
|
+
<option value="custom">custom</option>
|
|
303
|
+
</select>
|
|
304
|
+
<small class="text-muted">Node 侧将对节点聚合与扩散值应用该激活</small>
|
|
305
|
+
<textarea id="activationCustom" class="form-control mt-2" rows="3" style="display:none;" placeholder="Custom activation JS. Example: return x > 0 ? x : 0;"></textarea>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="col-md-6">
|
|
308
|
+
<label for="transferType" class="form-label">Transfer Function</label>
|
|
309
|
+
<select id="transferType" class="form-select">
|
|
310
|
+
<option value="linear" selected>linear</option>
|
|
311
|
+
<option value="exp">exp</option>
|
|
312
|
+
<option value="inverse">inverse</option>
|
|
313
|
+
<option value="capped">capped</option>
|
|
314
|
+
<option value="custom">custom</option>
|
|
315
|
+
</select>
|
|
316
|
+
<small class="text-muted">传递函数决定沿边传播的信号衰减方式</small>
|
|
317
|
+
<textarea id="transferCustom" class="form-control mt-2" rows="3" style="display:none;" placeholder="Custom transfer JS. Args: value, weight, decayK, ctx. Example: return value * Math.exp(-(decayK*weight));"></textarea>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="d-flex justify-content-between mt-4">
|
|
322
|
+
<button type="submit" class="btn btn-primary">应用参数</button>
|
|
323
|
+
<button type="button" id="resetParams" class="btn btn-secondary">重置默认值</button>
|
|
324
|
+
</div>
|
|
288
325
|
|
|
289
326
|
</form>
|
|
290
327
|
</div>
|
|
@@ -534,6 +571,7 @@ app.get('/', (req, res) => {
|
|
|
534
571
|
document.getElementById('confirmDeleteSnapshot').addEventListener('click', function() {
|
|
535
572
|
deleteSnapshot();
|
|
536
573
|
});
|
|
574
|
+
bindActivationEditors(); // 新增:绑定选择器与自定义编辑显隐
|
|
537
575
|
});
|
|
538
576
|
|
|
539
577
|
// 发送消息
|
|
@@ -595,6 +633,8 @@ app.get('/', (req, res) => {
|
|
|
595
633
|
}
|
|
596
634
|
|
|
597
635
|
// 加载模型参数
|
|
636
|
+
|
|
637
|
+
// 加载模型参数(保持接口不变)
|
|
598
638
|
function loadModelParams() {
|
|
599
639
|
fetch('/api/model/params')
|
|
600
640
|
.then(response => response.json())
|
|
@@ -672,6 +712,13 @@ function updateParamSliders(params) {
|
|
|
672
712
|
document.getElementById('waitingTimeRange').value = params.waitingTime;
|
|
673
713
|
document.getElementById('waitingTimeValue').textContent = params.waitingTime;
|
|
674
714
|
}
|
|
715
|
+
if (params.activationType) document.getElementById('activationType').value = params.activationType;
|
|
716
|
+
if (params.transferType) document.getElementById('transferType').value = params.transferType;
|
|
717
|
+
if (typeof params.activationCustom === 'string') document.getElementById('activationCustom').value = params.activationCustom;
|
|
718
|
+
if (typeof params.transferCustom === 'string') document.getElementById('transferCustom').value = params.transferCustom;
|
|
719
|
+
|
|
720
|
+
// 触发一次显隐同步
|
|
721
|
+
if (typeof bindActivationEditors === 'function') bindActivationEditors();
|
|
675
722
|
} // 添加这个花括号
|
|
676
723
|
|
|
677
724
|
function bindSliderEvents() {
|
|
@@ -705,43 +752,47 @@ function bindSliderEvents() {
|
|
|
705
752
|
});
|
|
706
753
|
}
|
|
707
754
|
// 更新模型参数
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
755
|
+
// 更新模型参数:增加激活/传递函数字段
|
|
756
|
+
function updateModelParams() {
|
|
757
|
+
const params = {
|
|
758
|
+
decayFactor: parseFloat(document.getElementById('decayFactorRange').value),
|
|
759
|
+
maxMemeWords: parseInt(document.getElementById('maxMemeWordsRange').value),
|
|
760
|
+
minOverlapThreshold: parseInt(document.getElementById('minOverlapThresholdRange').value),
|
|
761
|
+
maliciousThreshold: parseFloat(document.getElementById('maliciousThresholdRange').value),
|
|
762
|
+
learningIterations: parseInt(document.getElementById('learningIterationsRange').value),
|
|
763
|
+
iteration: parseInt(document.getElementById('iterationRange').value),
|
|
764
|
+
threshold: parseInt(document.getElementById('thresholdRange').value),
|
|
765
|
+
decay: parseFloat(document.getElementById('decayRange').value),
|
|
766
|
+
decayK: parseFloat(document.getElementById('decayKRange').value),
|
|
767
|
+
maxLen: parseInt(document.getElementById('maxLenRange').value),
|
|
768
|
+
edgeWeight: parseFloat(document.getElementById('edgeWeightRange').value),
|
|
769
|
+
communicateCount: parseInt(document.getElementById('communicateCountRange').value),
|
|
770
|
+
waitingTime: parseInt(document.getElementById('waitingTime').value),
|
|
771
|
+
// 新增
|
|
772
|
+
activationType: document.getElementById('activationType').value,
|
|
773
|
+
transferType: document.getElementById('transferType').value,
|
|
774
|
+
activationCustom: document.getElementById('activationCustom').value,
|
|
775
|
+
transferCustom: document.getElementById('transferCustom').value
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
fetch('/api/model/params', {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
headers: { 'Content-Type': 'application/json' },
|
|
781
|
+
body: JSON.stringify(params),
|
|
782
|
+
})
|
|
783
|
+
.then(response => response.json())
|
|
784
|
+
.then(data => {
|
|
785
|
+
if (data.success) {
|
|
786
|
+
alert('参数更新成功!');
|
|
787
|
+
} else {
|
|
788
|
+
alert('参数更新失败: ' + (data.error || '未知错误'));
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
.catch(error => {
|
|
792
|
+
console.error('Error updating parameters:', error);
|
|
793
|
+
alert('更新参数时发生错误: ' + error.message);
|
|
794
|
+
});
|
|
738
795
|
}
|
|
739
|
-
})
|
|
740
|
-
.catch(error => {
|
|
741
|
-
console.error('Error updating parameters:', error);
|
|
742
|
-
alert('更新参数时发生错误: ' + error.message);
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
796
|
|
|
746
797
|
// 重置模型参数
|
|
747
798
|
function resetModelParams() {
|
|
@@ -776,6 +827,19 @@ function updateModelParams() {
|
|
|
776
827
|
.catch(error => console.error('Error loading system status:', error));
|
|
777
828
|
}
|
|
778
829
|
|
|
830
|
+
function bindActivationEditors() {
|
|
831
|
+
const actSel = document.getElementById('activationType');
|
|
832
|
+
const actTxt = document.getElementById('activationCustom');
|
|
833
|
+
const trSel = document.getElementById('transferType');
|
|
834
|
+
const trTxt = document.getElementById('transferCustom');
|
|
835
|
+
const sync = () => {
|
|
836
|
+
actTxt.style.display = (actSel.value === 'custom') ? '' : 'none';
|
|
837
|
+
trTxt.style.display = (trSel.value === 'custom') ? '' : 'none';
|
|
838
|
+
};
|
|
839
|
+
actSel.addEventListener('change', sync);
|
|
840
|
+
trSel.addEventListener('change', sync);
|
|
841
|
+
sync();
|
|
842
|
+
}
|
|
779
843
|
function updateParamSliders(params) {
|
|
780
844
|
// 定义一个更新滑块的辅助函数
|
|
781
845
|
function updateSlider(paramName, sliderId, valueId) {
|
|
@@ -1040,13 +1104,6 @@ function updateParamSliders(params) {
|
|
|
1040
1104
|
`);
|
|
1041
1105
|
});
|
|
1042
1106
|
|
|
1043
|
-
// 目标端口列表
|
|
1044
|
-
const AI_PORTS = [
|
|
1045
|
-
process.env.AI_PORT_A || process.argv[3],
|
|
1046
|
-
process.env.AI_PORT_B || process.argv[4],
|
|
1047
|
-
process.env.AI_PORT_C || process.argv[5]
|
|
1048
|
-
];
|
|
1049
|
-
const Study_Port=process.env.AI_STUDY_PORT || process.argv[6]; // 学习模块端口
|
|
1050
1107
|
|
|
1051
1108
|
const systemStats = {
|
|
1052
1109
|
requestsTotal: 0,
|
|
@@ -1057,7 +1114,167 @@ const systemStats = {
|
|
|
1057
1114
|
aiResponseTimes: { [process.argv[3]]: [], [process.argv[4]]: [], [process.argv[5]]: [] },
|
|
1058
1115
|
lastErrors: []
|
|
1059
1116
|
};
|
|
1117
|
+
// ...existing code...
|
|
1118
|
+
|
|
1119
|
+
// ========== 分片/冷-热池调度(forwarder 层) ==========
|
|
1120
|
+
class ShardDescriptor {
|
|
1121
|
+
/**
|
|
1122
|
+
* @param {Object} opts
|
|
1123
|
+
* @param {string} opts.id 逻辑ID(如 "text_base", "law_1")
|
|
1124
|
+
* @param {number[]} opts.ports 该 shard 对应的后端端口列表(当前版本:本 forwarder 下的某个子集)
|
|
1125
|
+
* @param {number[]} [opts.embedding] 语义中心向量(可选,先空)
|
|
1126
|
+
* @param {string[]} [opts.tags] 领域标签,如 ['code','cn','law']
|
|
1127
|
+
*/
|
|
1128
|
+
constructor(opts) {
|
|
1129
|
+
this.id = opts.id;
|
|
1130
|
+
this.ports = opts.ports || [];
|
|
1131
|
+
this.embedding = Array.isArray(opts.embedding) ? opts.embedding : null;
|
|
1132
|
+
this.tags = Array.isArray(opts.tags) ? opts.tags : [];
|
|
1133
|
+
this.lastUsedTs = 0;
|
|
1134
|
+
this.usageCount = 0;
|
|
1135
|
+
this.loaded = true; // 第1轮简化:都视为“热”,仅做路由选择
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
touch() {
|
|
1139
|
+
this.lastUsedTs = Date.now();
|
|
1140
|
+
this.usageCount++;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// 非TF的极简 embedding:词袋hash到固定维度
|
|
1145
|
+
function hashStrSimple(str, seed = 1315423911) {
|
|
1146
|
+
let h = seed >>> 0;
|
|
1147
|
+
for (let i = 0; i < str.length; i++) {
|
|
1148
|
+
h ^= ((h << 5) + str.charCodeAt(i) + (h >>> 2)) >>> 0;
|
|
1149
|
+
}
|
|
1150
|
+
return h >>> 0;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function textToMiniEmbedding(text, dim = 64) {
|
|
1154
|
+
const vec = new Float32Array(dim);
|
|
1155
|
+
const toks = basicClean(text).toLowerCase().split(/[^a-z0-9_\-\u4e00-\u9fa5]+/).filter(Boolean);
|
|
1156
|
+
if (!toks.length) return Array.from(vec);
|
|
1157
|
+
for (const t of toks) {
|
|
1158
|
+
const h = hashStrSimple(t);
|
|
1159
|
+
const idx = h % dim;
|
|
1160
|
+
vec[idx] += 1;
|
|
1161
|
+
}
|
|
1162
|
+
// L2 normalize
|
|
1163
|
+
let n2 = 0; for (let i = 0; i < dim; i++) n2 += vec[i] * vec[i];
|
|
1164
|
+
n2 = Math.sqrt(n2) || 1;
|
|
1165
|
+
for (let i = 0; i < dim; i++) vec[i] /= n2;
|
|
1166
|
+
return Array.from(vec);
|
|
1167
|
+
}
|
|
1060
1168
|
|
|
1169
|
+
function cosineSim(a, b) {
|
|
1170
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
1171
|
+
let dot = 0, na = 0, nb = 0;
|
|
1172
|
+
for (let i = 0; i < a.length; i++) {
|
|
1173
|
+
dot += a[i] * b[i];
|
|
1174
|
+
na += a[i] * a[i];
|
|
1175
|
+
nb += b[i] * b[i];
|
|
1176
|
+
}
|
|
1177
|
+
if (!na || !nb) return 0;
|
|
1178
|
+
return dot / Math.sqrt(na * nb);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
class ShardManager {
|
|
1182
|
+
constructor(allPorts) {
|
|
1183
|
+
this.shards = new Map(); // id -> ShardDescriptor
|
|
1184
|
+
this.portToShard = new Map(); // port -> shardId(方便反查)
|
|
1185
|
+
this.dim = 64;
|
|
1186
|
+
|
|
1187
|
+
// 初始策略:把当前 AI_PORTS 按顺序平均分成几组,构成 “模型组”
|
|
1188
|
+
// 例如 3 个端口 => 一个 shard;12 个端口 => 3~4 个 shard
|
|
1189
|
+
const ports = allPorts.filter(p => !!p).map(p => Number(p));
|
|
1190
|
+
const N = ports.length;
|
|
1191
|
+
if (!N) return;
|
|
1192
|
+
const targetShardCount = Math.min(4, Math.max(1, Math.floor(N / 3))) || 1;
|
|
1193
|
+
const shardSize = Math.max(1, Math.floor(N / targetShardCount));
|
|
1194
|
+
let idx = 0;
|
|
1195
|
+
for (let s = 0; s < targetShardCount; s++) {
|
|
1196
|
+
const slice = ports.slice(idx, idx + shardSize);
|
|
1197
|
+
idx += shardSize;
|
|
1198
|
+
if (!slice.length) break;
|
|
1199
|
+
const id = `shard_${s}`;
|
|
1200
|
+
const desc = new ShardDescriptor({
|
|
1201
|
+
id,
|
|
1202
|
+
ports: slice,
|
|
1203
|
+
embedding: null,
|
|
1204
|
+
tags: [] // 可以后续通过API补充
|
|
1205
|
+
});
|
|
1206
|
+
this.shards.set(id, desc);
|
|
1207
|
+
for (const p of slice) this.portToShard.set(p, id);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
listShards() {
|
|
1212
|
+
return Array.from(this.shards.values()).map(s => ({
|
|
1213
|
+
id: s.id,
|
|
1214
|
+
ports: s.ports,
|
|
1215
|
+
tags: s.tags,
|
|
1216
|
+
loaded: s.loaded,
|
|
1217
|
+
lastUsedTs: s.lastUsedTs,
|
|
1218
|
+
usageCount: s.usageCount
|
|
1219
|
+
}));
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// 手工更新某个 shard 的语义中心 + 标签
|
|
1223
|
+
updateShardMeta(id, { embedding, tags } = {}) {
|
|
1224
|
+
const s = this.shards.get(id);
|
|
1225
|
+
if (!s) return false;
|
|
1226
|
+
if (Array.isArray(embedding)) s.embedding = embedding.slice();
|
|
1227
|
+
if (Array.isArray(tags)) s.tags = tags.slice();
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* 根据当前对话 embedding + 可选 tags,选出本轮要用的 shard 列表
|
|
1233
|
+
* @param {number[]} queryEmb
|
|
1234
|
+
* @param {Object} opt
|
|
1235
|
+
* @param {number} opt.topK 选多少个 shard
|
|
1236
|
+
* @param {string[]} [opt.hints] 额外提示(如 'code','zh')
|
|
1237
|
+
*/
|
|
1238
|
+
selectShards(queryEmb, { topK = 2, hints = [] } = {}) {
|
|
1239
|
+
const entries = Array.from(this.shards.values()).filter(s => s.loaded && s.ports.length);
|
|
1240
|
+
if (!entries.length) return [];
|
|
1241
|
+
|
|
1242
|
+
const scores = entries.map(s => {
|
|
1243
|
+
let sim = 0;
|
|
1244
|
+
if (s.embedding) sim = cosineSim(queryEmb, s.embedding);
|
|
1245
|
+
let tagBonus = 0;
|
|
1246
|
+
if (hints && hints.length && s.tags && s.tags.length) {
|
|
1247
|
+
const inter = s.tags.filter(t => hints.includes(t));
|
|
1248
|
+
tagBonus = inter.length ? 0.1 * inter.length : 0;
|
|
1249
|
+
}
|
|
1250
|
+
// 加一点近期使用度的温度
|
|
1251
|
+
const usageBoost = Math.log(1 + s.usageCount) * 0.01;
|
|
1252
|
+
return { shard: s, score: sim + tagBonus + usageBoost };
|
|
1253
|
+
});
|
|
1254
|
+
scores.sort((a, b) => b.score - a.score);
|
|
1255
|
+
const out = scores.slice(0, Math.max(1, topK)).map(x => x.shard);
|
|
1256
|
+
const now = Date.now();
|
|
1257
|
+
for (const s of out) { s.lastUsedTs = now; s.usageCount++; }
|
|
1258
|
+
return out;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// 当前端口属于哪个 shard(用于监控)
|
|
1262
|
+
shardOfPort(port) {
|
|
1263
|
+
const id = this.portToShard.get(Number(port));
|
|
1264
|
+
return id || null;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// 初始化 ShardManager
|
|
1269
|
+
const AI_PORTS = [
|
|
1270
|
+
process.env.AI_PORT_A || process.argv[3],
|
|
1271
|
+
process.env.AI_PORT_B || process.argv[4],
|
|
1272
|
+
process.env.AI_PORT_C || process.argv[5]
|
|
1273
|
+
];
|
|
1274
|
+
const Study_Port = process.env.AI_STUDY_PORT || process.argv[6];
|
|
1275
|
+
|
|
1276
|
+
const shardManager = new ShardManager(AI_PORTS);
|
|
1277
|
+
// ========== 冷/热池调度结束 ==========
|
|
1061
1278
|
// 新增:统一从各后端响应中提取文本
|
|
1062
1279
|
function extractText(resp) {
|
|
1063
1280
|
if (!resp) return '';
|
|
@@ -1080,7 +1297,12 @@ const modelDefaults = {
|
|
|
1080
1297
|
minOverlapThreshold: 2,
|
|
1081
1298
|
maliciousThreshold: 0.7,
|
|
1082
1299
|
learningIterations: 3,
|
|
1083
|
-
communicateCount: 1
|
|
1300
|
+
communicateCount: 1,
|
|
1301
|
+
// 新增:激活与传递函数选择
|
|
1302
|
+
activationType: 'relu',
|
|
1303
|
+
transferType: 'linear',
|
|
1304
|
+
activationCustom: '',
|
|
1305
|
+
transferCustom: ''
|
|
1084
1306
|
};
|
|
1085
1307
|
|
|
1086
1308
|
// 当前应用的模型参数
|
|
@@ -1100,28 +1322,30 @@ function perturb(arr) {
|
|
|
1100
1322
|
*/
|
|
1101
1323
|
// 修改 requestAI,返回 null 表示离线
|
|
1102
1324
|
// 修改 requestAI 函数,增加重试和超时处理
|
|
1103
|
-
async function requestAI(port, message, retries = 3) {
|
|
1325
|
+
async function requestAI(port, message, retries = 3, shardId = null) {
|
|
1104
1326
|
const url = `http://localhost:${port}/api/chat`;
|
|
1105
1327
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1106
1328
|
try {
|
|
1107
1329
|
const start = Date.now();
|
|
1108
|
-
const response = await axios.post(url, { message }, { timeout: 10000 });
|
|
1330
|
+
const response = await axios.post(url, { message }, { timeout: 10000 });
|
|
1109
1331
|
const latency = Date.now() - start;
|
|
1110
1332
|
systemStats.aiResponseTimes[port] = systemStats.aiResponseTimes[port] || [];
|
|
1111
1333
|
systemStats.aiResponseTimes[port].push(latency);
|
|
1334
|
+
// 这里可以将 shardId 写入统计日志,但先不动 API
|
|
1112
1335
|
return response.data;
|
|
1113
1336
|
} catch (error) {
|
|
1114
|
-
console.warn(`[WARN] 请求 AI 实例 ${port} 失败 (尝试 ${attempt}/${retries}): ${error.message}`);
|
|
1337
|
+
console.warn(`[WARN] 请求 AI 实例 ${port} (shard=${shardId || 'unknown'}) 失败 (尝试 ${attempt}/${retries}): ${error.message}`);
|
|
1115
1338
|
if (attempt === retries) {
|
|
1116
1339
|
systemStats.lastErrors.push({ port, error: error.message });
|
|
1117
|
-
return null;
|
|
1340
|
+
return null;
|
|
1118
1341
|
}
|
|
1119
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1342
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1120
1343
|
}
|
|
1121
1344
|
}
|
|
1122
1345
|
}
|
|
1123
1346
|
|
|
1124
1347
|
|
|
1348
|
+
// ...existing code...
|
|
1125
1349
|
app.post('/api/chat', async (req, res) => {
|
|
1126
1350
|
const { message } = req.body;
|
|
1127
1351
|
if (!message) return res.status(400).json({ error: 'No message' });
|
|
@@ -1136,27 +1360,49 @@ app.post('/api/chat', async (req, res) => {
|
|
|
1136
1360
|
let results = [];
|
|
1137
1361
|
|
|
1138
1362
|
try {
|
|
1139
|
-
//
|
|
1363
|
+
// === 核心:为当前请求生成embedding并选择合适 shard ===
|
|
1364
|
+
const queryEmb = textToMiniEmbedding(message, shardManager.dim);
|
|
1365
|
+
// 可加一点简单的“领域 hint”:例如包含 code / law / zh 等关键词
|
|
1366
|
+
const hints = [];
|
|
1367
|
+
const lower = message.toLowerCase();
|
|
1368
|
+
if (/[{};()=]/.test(message) || /code|function|class|import|def /.test(lower)) hints.push('code');
|
|
1369
|
+
if (/[,。?!]/.test(message) || /的|了|吗/.test(message)) hints.push('zh');
|
|
1370
|
+
// 暂时不细分,topK=2
|
|
1371
|
+
const selectedShards = shardManager.selectShards(queryEmb, { topK: 2, hints });
|
|
1372
|
+
|
|
1373
|
+
// 从被选中的 shard 中收集端口
|
|
1374
|
+
let candidatePorts = [];
|
|
1375
|
+
for (const s of selectedShards) candidatePorts.push(...s.ports);
|
|
1376
|
+
// 去重复
|
|
1377
|
+
candidatePorts = Array.from(new Set(candidatePorts));
|
|
1378
|
+
|
|
1379
|
+
// 如果还为空,兜底用全部 AI_PORTS
|
|
1380
|
+
if (!candidatePorts.length) {
|
|
1381
|
+
candidatePorts = AI_PORTS.filter(p => !!p);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// 星火阵列多轮交互(每一轮并发请求若干 AI)
|
|
1140
1385
|
for (let round = 0; round <= communicateCount; round++) {
|
|
1386
|
+
const msgArr = inputs.map(arr => (arr || words).join(' '));
|
|
1387
|
+
// 对不超过 candidatePorts.length 的前几个输入进行并发请求
|
|
1141
1388
|
results = await Promise.all(
|
|
1142
|
-
|
|
1389
|
+
candidatePorts.map((port, idx) =>
|
|
1390
|
+
requestAI(port, msgArr[idx % msgArr.length], 3, shardManager.shardOfPort(port))
|
|
1391
|
+
)
|
|
1143
1392
|
);
|
|
1144
1393
|
|
|
1145
|
-
// 下一轮输入为本轮输出分词(先提取文本,再 split)
|
|
1146
1394
|
if (round < communicateCount) {
|
|
1147
|
-
console.log(results);
|
|
1148
1395
|
inputs = results.map(r => extractText(r).toLowerCase().split(/\s+/).filter(Boolean));
|
|
1149
1396
|
}
|
|
1150
1397
|
}
|
|
1151
1398
|
|
|
1152
|
-
// 只保留有效响应(提取为字符串)
|
|
1153
1399
|
const texts = results.map(extractText).filter(t => typeof t === 'string' && t.length > 0);
|
|
1154
1400
|
if (texts.length === 0) {
|
|
1155
1401
|
systemStats.requestsFailed++;
|
|
1156
1402
|
return res.status(502).json({ error: '所有AI实例均不可用或无响应' });
|
|
1157
1403
|
}
|
|
1158
1404
|
|
|
1159
|
-
//
|
|
1405
|
+
// ...后面词频融合 + SERIALIZER_API 部分保持不变...
|
|
1160
1406
|
const freq = {};
|
|
1161
1407
|
texts.forEach(t => t.split(/\s+/).forEach(w => { if (w) freq[w] = (freq[w] || 0) + 1; }));
|
|
1162
1408
|
const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
|
|
@@ -1178,7 +1424,18 @@ app.post('/api/chat', async (req, res) => {
|
|
|
1178
1424
|
res.status(500).json({ error: err.message });
|
|
1179
1425
|
}
|
|
1180
1426
|
});
|
|
1427
|
+
app.get('/api/shards', (req, res) => {
|
|
1428
|
+
res.json({ ok: true, shards: shardManager.listShards() });
|
|
1429
|
+
});
|
|
1181
1430
|
|
|
1431
|
+
// 更新某个 shard 的标签/embedding(embedding 可手工填或从文件导入)
|
|
1432
|
+
app.post('/api/shards/:id/meta', (req, res) => {
|
|
1433
|
+
const { id } = req.params;
|
|
1434
|
+
const { embedding, tags } = req.body || {};
|
|
1435
|
+
const ok = shardManager.updateShardMeta(id, { embedding, tags });
|
|
1436
|
+
if (!ok) return res.status(404).json({ ok: false, error: 'shard not found' });
|
|
1437
|
+
res.json({ ok: true });
|
|
1438
|
+
});
|
|
1182
1439
|
//==============================================================================
|
|
1183
1440
|
// 新增功能:模型参数调节
|
|
1184
1441
|
//==============================================================================
|