/
yiyan.js
1418 lines (1202 loc) · 46.6 KB
/
yiyan.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
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
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
////////////////// 虎绿林 文心一言 聊天机器人 //////////////////
/**************************************************************
使用方法:
1. 使用最新版的Chrome谷歌浏览器或Firefox火狐浏览器,不要使用QQ浏览器、360浏览器等,不保证兼容。
2. 安装油猴插件:https://www.tampermonkey.net/
3. 在油猴里添加新脚本,粘贴如下代码并保存:
// ==UserScript==
// @name 虎绿林文心一言机器人
// @namespace https://hu60.cn/
// @version 1.0
// @description 把文心一言接入hu60wap6网站程序
// @author 老虎会游泳
// @match https://yiyan.baidu.com/
// @icon https://hu60.cn/favicon.ico
// @grant none
// ==/UserScript==
document.hu60VConsole = false; // 是否显示调试控制台,false:隐藏;true:显示。
document.hu60User = ''; // 虎绿林用户名
document.hu60Pwd = ''; // 虎绿林密码
document.hu60AdminUids = [1, 19346, 15953]; // 机器人管理员uid,管理员可以发“@文心一言,刷新页面”来重启机器人
document.hu60Domain = 'https://hu60.cn'; // 如果要对接其他网站,请修改此处的域名(必须是https的否则连不上)
var script = document.createElement("script");
script.src = document.hu60Domain + '/tpl/jhin/js/chatgpt/yiyan.js?r=' + (new Date().getTime());
document.head.appendChild(script);
4. 打开 https://yiyan.baidu.com/ 并登录。
5. 在来到聊天页面时,会弹出输入虎绿林用户名密码的提示框。
如果你要把机器人接入虎绿林,请注册一个新帐号。**使用现有帐号运行机器人将被删帖或禁言**。
输入新帐号用户名密码后,机器人即启动,保持页面不要关闭。
机器人会使用你在此处输入的帐号与其他用户进行对话,在虎绿林用其他帐号`@该帐号`即可尝试对话。
注意,使用该帐号自己`@自己`是不会有反应的,必须用另一个账号来和机器人对话。
6. 如果要打开F12开发者控制台,必须在“调试器”或“源代码”页面点击“停用断点”按钮(右上角的=/=>图标)并刷新,否则百度会暂停页面的运行。
7. 如何切换登录的帐号?按F12打开开发者工具,点“控制台”或“Console”,然后输入以下代码并回车:
login(true)
将会重新弹出用户名密码输入框。
8. 也可以把用户名密码填在油猴脚本里,这样就不用在对话框里输入了。
### 如何把机器人接入其他类型的网站?
你可以在油猴脚本的末尾添加一个自定义主循环,用于把机器人接入其他类型的网站。以下是一个例子:
document.run = async function() {
while (true) {
try {
// 访问你的网站获取要发给文心一言的内容
// 网站必须是https的,否则连不上。
// 此外网站还必须设置 Access-Control-Allow-Origin: * 头信息,否则也连不上。
let response = await fetch('https://example.com/my-message.php');
// 假设获取到的信息是JSON,把它转换成JSON对象
// 网站必须设置 content-type: application/json 头信息,否则转换会失败。
let messages = response.json();
// 假设JSON结构是这样:
// {"data": [
// {"uid":3, "text":"@文心一言,你好"},
// {"uid":2, "text":"@文心一言,我有一个问题"},
// {"uid":1, "text":"@文心一言,刷新页面"},
// ]}
let exceptionCount = 0;
for (let i=0; i<messages.data.length; i++) {
// 要发给文心一言的话,开头包含的“@机器人名称,”会被后续流程自动去除。
let text = messages.data.text;
// 用户id,可以是字符串,所以给出用户名也是可以的。
let uid = messages.data.uid;
try {
// 把对话发给文心一言
// 返回的 modelIndex 是为对话选择的模型id(从0开始编号),目前始终是0
let modelIndex = await sendRequest(text, uid);
// 从文心一言读取回复
let replyText = await readReply();
// 发送回复到你的网站
// 创建一个POST表单
let formData = new FormData();
formData.append('token', '用于用户身份验证的密钥');
formData.append('reply', replyText); // 回复内容
// 提交POST表单
// 网站必须是https的,否则连不上。
// 此外网站还必须设置 Access-Control-Allow-Origin: * 头信息,否则也连不上。
let response = await fetch('https://example.com/my-reply.php', {
body: formData,
method: "post",
redirect: "manual" // 不自动重定向
});
// 在控制台打印提交结果
if (response.type == 'opaqueredirect') {
console.log('提交后收到重定向(目标网址未知,根据标准,浏览器不告诉我们),不清楚提交是否成功');
} else {
let result = await response.text();
console.log('提交结果', result);
}
// 避免操作太快
await sleep(100);
} catch (ex) {
exceptionCount++; // 统计异常次数
console.error(ex); // 打印异常到控制台
await sleep(1000); // 异常后等久一点
}
// 重命名会话
await renameWant();
}
// 执行管理员命令(比如“刷新页面”)
await runAdminCommand();
// 异常太多,自动刷新页面
if (exceptionCount > 0 && exceptionCount >= messages.data.length) {
refreshPage();
await sleep(5000); // 防止实际刷新前执行到后面的代码
}
// 限制拉取信息的速度,避免对自己的网站造成CC攻击
await sleep(1000);
} catch (ex) {
console.error(ex);
await sleep(1000);
}
}
}
**************************************************************/
// 与之前的启动方式保持兼容
if (typeof hu60Domain != 'undefined') {
document.hu60Domain = hu60Domain;
}
// 虎绿林URL
const hu60Url = document.hu60Domain + '/q.php/';
// https://github.com/mixmark-io/turndown
// 老虎会游泳修改了 collapseWhitespace 函数以保留所有空白和换行
const turndownJsUrl = document.hu60Domain + '/tpl/jhin/js/chatgpt/turndown-tigermod.js';
// https://github.com/mixmark-io/turndown-plugin-gfm
const turndownGfmJsUrl = document.hu60Domain + '/tpl/jhin/js/chatgpt/turndown-plugin-gfm.js';
// 虚拟控制台
const vConsoleJsUrl = document.hu60Domain + '/tpl/jhin/js/chatgpt/vconsole.js?r=' + (new Date().getTime());
/////////////////////////////////////////////////////////////
// 错误提示翻译
const errorMap = {
'READ_REPLY_FAILED':
"读取回复出错,请重试。每天第一次和机器人对话时经常发生这种错误,通常再试一次就会好。",
};
// 错误提示文本的最大长度
const errorMaxLen = Math.max(...Object.keys(errorMap).map(x => x.length));
// 模型对应关系
const modelMap = {
1 : 0, // 文心一言没有模型切换功能,保留接口备用
};
/////////////////////////////////////////////////////////////
// 聊天框的CSS选择器
const chatBoxSelector = 'textarea.ant-input.wBs12eIN';
// 发送按钮的CSS选择器
const sendButtonSelector = 'span.VAtmtpqL';
// 正在输入动效(三个点)和加载中动效(转圈)的CSS选择器
const replyNotReadySelector = 'span.vIpm62hT';
// 停止生成/重新生成按钮
const stopOrRegenButtonSelector = 'span.yyjIo3Fm, span.mEKFkIX7';
// 聊天内容(包括提问与回复)的CSS选择器
const chatLineSelector = 'div.H7oUCk_o, div.custom-html';
// 聊天回答的CSS选择器
const chatReplySelector = 'div.custom-html';
// 左侧会话列表项的CSS选择器
const sessionListItemSelector = 'div.r1qZ7V8X';
// 当前会话的CSS选择器
const currentSessionSelector = 'div.r1qZ7V8X.SVPvtzwe';
// 编辑、删除、确认、取消按钮的CSS选择器
const actionButtonSelector = 'div.r1qZ7V8X.SVPvtzwe span.V4Z0LJMp, div.H2sWbmDH span.V4Z0LJMp';
// 删除确认按钮的CSS选择器
const deleteConfirmSelector = 'button.ant-btn-sm';
// 会话名称编辑框的CSS选择器
const sessionNameInputSelector = 'div.H2sWbmDH input.ant-input';
// 新建会话按钮的CSS选择器
const newChatButtonSelector = 'span.MO979HM2';
// 模型下拉框的CSS选择器(文心一言没有模型选择功能,保留此接口备用)
const modelListBoxSelector = 'hu60-none';
// 模型列表项的CSS选择器
const modelListItemSelector = 'hu60-none';
// “Upgrade to Plus”按钮的CSS选择器
const upgradeToPlusSelector = 'hu60-none';
// 会话列表“Show more”按钮的CSS选择器(尚未观察到该按钮)
const showMoreButtonSelector = 'hu60-none';
// 刷新按钮的CSS选择器
// “当前在线等待用户过多,你已长时间没有提问,请刷新重试”
const refreshButtonSelector = 'div.DsDCFSHH';
/////////////////////////////////////////////////////////////
// 在线机器人列表(自动获取)
var hu60OnlineBot = {};
// 用户自身的虎绿林uid(自动获取)
var hu60MyUid = null;
// 虎绿林sid
var hu60Sid = null;
// 带sid的虎绿林URL(自动获取)
var hu60BaseUrl = null;
// 在切换会话前重命名当前会话
// 缓解重命名失败的方法
var wantRename = null;
// 上次会话的名称
// 在会话历史记录功能不可用时减少不必要的新建会话
var lastSessionName = null;
// 回复结束时间
// 在回复结束2秒后重命名会话,
// 以防文心一言自动重命名会话导致我们的名称保存失败。
var replyFinishTime = 0;
// 管理员想要刷新页面
var wantRefresh = false;
// 新会话标识
var isNewSession = false;
// 模型名称
var modelName = null;
// 空白发言标识
var isTextEmpty = false;
// 命令短语回复
var commandPhraseReply = null;
// 指定回复中的代码高亮UBB
var replyCodeFormat = null;
var replyCodeFormatOpts = '';
// 重试对话内容缓存
var retryChatTexts = {};
// 上一条回复,用于防止获取到重复回复
var lastReply = null;
/////////////////////////////////////////////////////////////
// 命令短语
const commandPhrases = {
'结束会话' : async function(text, uid, modelIndex) {
if (isNewSession) {
commandPhraseReply = '会话未开始';
isNewSession = false;
wantRename = null;
} else {
commandPhraseReply = '会话已结束';
await deleteSession();
}
},
'刷新页面' : async function(text, uid, modelIndex) {
if (!document.hu60AdminUids || !document.hu60AdminUids.includes(uid)) {
commandPhraseReply = '您不是管理员,无法进行该操作';
return;
}
commandPhraseReply = '即将刷新页面';
wantRefresh = true;
wantRename = null;
},
'重试' : async function(text, uid, modelIndex) {
text = retryChatTexts[uid];
if (text === undefined || text === '重试') {
commandPhraseReply = '找不到可重试的发言';
return;
}
await sendText(text, uid, modelIndex);
}
};
// 执行管理员命令
// 为什么要定义成单独的函数?因为刷新操作需要在发送回复给管理员后再执行,
// 否则页面刷新了就没办法发送回复了。
async function runAdminCommand() {
// 重命名对话
await renameWant();
// 刷新页面
if (wantRefresh) {
refreshPage();
await sleep(5000); // 防止实际刷新前执行到后面的代码
wantRefresh = false;
}
}
/////////////////////////////////////////////////////////////
// 刷新页面
function refreshPage() {
console.error('刷新页面', Error().stack);
location.reload();
}
// 休眠指定的毫秒数
// 用法:await sleep(1000)
const sleep = ms => new Promise(r => setTimeout(r, ms));
// 加载外部js
function loadScript(url) {
var script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
}
// Changing a React Input Value from Vanilla Javascript
// 通过原生js修改React输入框的值
// From: <https://chuckconway.com/changing-a-react-input-value-from-vanilla-javascript/>
function setNativeValue(element, value) {
let lastValue = element.value;
element.value = value;
let event = new Event("input", { target: element, bubbles: true });
// React 15
event.simulated = true;
// React 16
let tracker = element._valueTracker;
if (tracker) {
tracker.setValue(lastValue);
}
element.dispatchEvent(event);
}
// 模拟点击
function sendClickEvent(element) {
let event = new Event("click", { target: element, bubbles: true });
event.simulated = true;
element.dispatchEvent(event);
}
/////////////////////////////////////////////////////////////
// 选择模型
async function selectModel(modelIndex) {
if (!document.querySelector(modelListBoxSelector)) {
// 文心一言没有模型选择功能,保留此接口备用
return;
}
// 等待模型选择器出现
for (let i=0; i<50 && !document.querySelector(modelListBoxSelector); i++) {
await sleep(100);
}
let box = document.querySelector(modelListBoxSelector);
if (!box) {
// 找不到模型选择器
return;
}
let models = document.querySelectorAll(modelListItemSelector);
if (models.length < 2) {
// 弹出模型下拉框
box.click();
await sleep(100);
for (let i=0; i<10 && document.querySelectorAll(modelListItemSelector).length < 2; i++) {
await sleep(100);
}
models = document.querySelectorAll(modelListItemSelector);
}
// 选择模型
if (modelIndex < models.length) {
console.log("selectModel", modelIndex, models[modelIndex].innerText);
models[modelIndex].click();
await sleep(100);
}
}
// 创建新会话
async function newChatSession(name, modelIndex) {
let sessionIndex = getSessions().length + 1;
console.log('newChatSession', sessionIndex, modelIndex, 'begin');
document.querySelector(newChatButtonSelector).click();
// 等待新建完成
let i = 0;
do {
await sleep(100);
i++;
} while (
( !document.querySelector(chatBoxSelector)
|| !document.querySelector(sendButtonSelector))
&& i < 100
);
// 再多等一会儿,防止意外
await sleep(100);
// 选择模型
await selectModel(modelIndex);
isNewSession = true;
wantRename = name;
console.log('newChatSession', sessionIndex, modelIndex, 'end');
}
// 删除当前会话
async function deleteSession() {
try {
// 会话不存在,无需删除
if (!getCurrentSession()) {
return;
}
let sessionNum = getSessions().length;
console.log('deleteSession', 'begin', sessionNum);
let actionButtons = document.querySelectorAll(actionButtonSelector);
if (!actionButtons[1]) {
throw "找不到删除按钮";
}
// onclick事件绑定在svg上
let svg = actionButtons[1].querySelector('svg');
svg.focus();
await sleep(100);
// 点击删除按钮
sendClickEvent(svg);
await sleep(100);
actionButtons = document.querySelectorAll(deleteConfirmSelector);
if (!actionButtons[1]) {
throw "找不到确认按钮";
}
actionButtons[1].click(); // 点击确认按钮
// 等待删除完成
for (let i=0; i<100 && getSessions().length >= sessionNum; i++) {
await sleep(100);
}
isNewSession = false;
wantRename = null;
console.log('deleteSession', 'end', getSessions().length);
} catch (ex) {
console.error('会话删除失败', ex);
if (commandPhraseReply) {
commandPhraseReply = "会话删除失败:" + ex + "\n\n@老虎会游泳,机器人代码需要更新。";
}
}
}
// 重命名会话
async function renameSession(newName) {
try {
// 等待加载完成
for (let i=0; i<100 && (!isFinished() || !getCurrentSession()); i++) {
await sleep(100);
}
getCurrentSession().click();
await sleep(100);
let actionButtons = document.querySelectorAll(actionButtonSelector);
if (!actionButtons[0]) {
console.error('renameSession', '找不到编辑按钮');
return;
}
actionButtons[0].click(); // 点击编辑按钮
await sleep(100);
let nameInput = document.querySelector(sessionNameInputSelector);
if (!nameInput) {
console.error('renameSession', '找不到输入框');
return;
}
// 输入框获取焦点
nameInput.focus();
await sleep(100);
// 设置输入框的值
setNativeValue(nameInput, newName);
await sleep(100);
actionButtons = document.querySelectorAll(actionButtonSelector);
if (!actionButtons[0]) {
console.error('renameSession', '找不到确认按钮');
return;
}
actionButtons[0].click(); // 点击确认按钮
await sleep(100);
} catch (ex) {
console.error('会话重命名失败', ex);
}
}
// 获取会话列表
function getSessions() {
return document.querySelectorAll(sessionListItemSelector);
}
// 查找会话
async function findSession(name) {
// 等待加载完成
for (let i=0; i<100 && !isFinished(); i++) {
await sleep(100);
}
let sessions = getSessions();
for (let i=0; i<sessions.length; i++) {
// 重命名时会交替使用.和-,有可能保存上的是.而非-
if (sessions[i].innerText.replace('.', '-') == name) {
return sessions[i];
}
}
return null;
}
// 获取当前session
function getCurrentSession() {
return document.querySelector(currentSessionSelector);
}
// 获取当前session的名称
function getSessionName() {
let session = getCurrentSession();
if (session) {
// 重命名时会交替使用.和-,有可能保存上的是.而非-
return session.innerText.replace('.', '-');
}
return null;
}
// 切换会话前重命名当前会话
// 缓解重命名失败的方法
async function renameWant() {
if (wantRename !== null) {
// 距离回复不到2秒,等够2秒
// 防止重命名过程中文心一言同时自动重命名,导致我们的名称保存失败
let timeDiff = 2000 - ((new Date().getTime()) - replyFinishTime);
if (timeDiff > 0) {
console.log(timeDiff + 'ms 后重命名会话');
await sleep(timeDiff);
}
await renameSession(wantRename);
wantRename = null;
}
}
// 切换会话
async function switchSession(name, modelIndex) {
isNewSession = false;
// 在会话历史记录功能不可用时减少不必要的新建会话
if (getSessions().length < 1 && lastSessionName === name) {
// 会话相同,无需切换
return;
}
// 需要切换会话,所以清理掉上次的会话名称
lastSessionName = null;
let stopOrRegenButton = document.querySelector(stopOrRegenButtonSelector);
if (stopOrRegenButton && stopOrRegenButton.textContent == '停止生成') {
// 会话生成卡住了,先点停止
stopOrRegenButton.click();
await sleep(500);
}
let session = await findSession(name);
if (!session) {
await renameWant();
return await newChatSession(name, modelIndex);
}
if (getCurrentSession() == session) {
if (document.querySelector(chatBoxSelector)
&& document.querySelector(sendButtonSelector)) {
// 无需切换
return;
} else {
// 找不到发言框,可能出错了,尝试新建一个会话
await deleteSession();
await newChatSession(name, modelIndex);
}
} else {
// 切换前先重命名当前会话
await renameWant();
}
console.log('switchSession', name, 'begin');
session.querySelector('span').click();
// 等待切换完成
let i = 0;
do {
await sleep(100);
i++;
} while (
(getSessionName() != name
|| !document.querySelector(chatBoxSelector)
|| !document.querySelector(sendButtonSelector)
|| !isFinished())
&& i < 100
);
// 再多等一会儿,防止意外
await sleep(100);
// 找不到发言框或发送按钮,当前会话可能出错
if (!document.querySelector(chatBoxSelector) || !document.querySelector(sendButtonSelector)) {
console.warn('找不到发言框或发送按钮,尝试删除会话', name);
await deleteSession();
return await newChatSession(name, modelIndex);
}
console.log('switchSession', name, 'end', i, getSessionName());
}
function makeSessionName(uid, modelIndex) {
return uid + '-' + modelIndex;
}
// 发送聊天信息
async function sendText(text, uid, modelIndex) {
try {
let commandFunc = commandPhrases[text];
// 保存重试内容
if (!commandFunc) {
retryChatTexts[uid] = text;
}
// 切换会话
let sessionName = makeSessionName(uid, modelIndex);
await switchSession(sessionName, modelIndex);
lastSessionName = sessionName;
// 等待加载完成
for (let i=0; i<100 && !isFinished(); i++) {
await sleep(100);
}
// 执行命令短语
if (commandFunc) {
return await commandFunc(text, uid, modelIndex);
}
if (text.length < 1) {
// 空白发言,用于取回上一条回复
return;
}
let chatBox, sendButton, lastChatLine;
lastReply = getLastReply();
let i = 0;
do {
chatBox = document.querySelector(chatBoxSelector);
sendButton = document.querySelector(sendButtonSelector);
// 输入框获取焦点
chatBox.focus();
await sleep(100);
// 设置输入框的值
setNativeValue(chatBox, text);
await sleep(100);
// 点击发送按钮
sendButton.click();
await sleep(100);
i++;
lastChatLine = getLastChatLine();
} while (i < 10 && chatBox && sendButton && lastChatLine &&
// 防止读取到上一条回复
lastChatLine.querySelector(chatReplySelector) === lastReply);
if (lastChatLine && lastChatLine.querySelector(chatReplySelector) === lastReply) {
throw '发言未上屏';
}
} catch (ex) {
wantRefresh = true;
console.error('发言失败', ex);
commandPhraseReply = '发言失败,请重试。当前会话已丢失。';
await deleteSession();
}
}
// 执行聊天信息中的指令
async function sendRequest(text, uid) {
// 等待现有任务完成
for (let i=0; i<1200 && !isFinished(); i++) {
await sleep(100);
}
console.log('sendRequest', '@#'+uid, text);
// 去除待审核提示
text = text.trim().replace(/^发言[^\s]+可见。[\r\n]+/s, '').trim();
// 分割指令
// @文心一言[ 模型序号][ 代码格式[=参数]],发言内容
// 示例:
// @文心一言,你好
// @文心一言 2,你好
// @文心一言 html,输出一段html hello world
// @文心一言 2 html,输出一段html hello world
// @文心一言 html=500,输出一段html hello world
// @文心一言 2 html=300x500,输出一段html hello world
let parts = text.match(/^(?:[\s,,::]*[@@][##a-zA-Z0-9_\-\p{Script=Han}]+)*(?:[\s,,::]+(\d+))?(?:[\s,,::]+(html|text|latex|math|raw)(=[0-9,x]+)?)?(?:[\s,,::]+(.*))?$/isu);
modelName = null;
replyCodeFormat = null;
replyCodeFormatOpts = '';
let modelIndex = modelMap[1];
if (parts) {
let model = parts[1];
let codeFormat = parts[2];
let codeFormatOpts = parts[3];
text = parts[4] || '';
// 选择模型
if (undefined !== model && undefined !== modelMap[Number(model)]) {
modelName = Number(model);
modelIndex = modelMap[modelName];
}
// 指定代码格式
if (undefined !== codeFormat) {
replyCodeFormat = codeFormat.toLowerCase();
replyCodeFormatOpts = codeFormatOpts || '';
}
}
isTextEmpty = (text.length == 0);
await sendText(text, uid, modelIndex);
return modelIndex;
}
// 文心一言的DOM排序是倒序,最新的在最前面
function getLastChatLine() {
return Array.from(document.querySelectorAll(chatLineSelector)).at(0);
}
// 文心一言的DOM排序是倒序,最新的在最前面
function getLastReply(index = 0) {
return Array.from(document.querySelectorAll(chatReplySelector)).at(index);
}
// 读取响应
async function readReply() {
if (commandPhraseReply) {
let reply = commandPhraseReply;
commandPhraseReply = null;
return reply;
}
// 等待回答完成
// 先等个1秒,防止过早读取,获取到上一条回复
await sleep(1000);
// 因为状态转换的瞬间存在错判,所以多等几轮,防止还没回复完就返回
for (let x=0; x<10; x++) {
let i = 0;
do {
await sleep(100);
i++;
} while (i<120 && !isFinished());
await sleep(100);
}
if (!isFinished()) {
// 发言卡住了,回复完成后自动刷新
wantRefresh = true;
}
replyFinishTime = new Date().getTime();
// 检查会话是否需要重命名
let sessionName = getSessionName();
if (sessionName != lastSessionName) {
wantRename = lastSessionName;
isNewSession = true;
}
// 加载 html 转 markdown 插件
let turndownService = null;
try {
if (typeof TurndownService == 'function') {
turndownService = new TurndownService({
'headingStyle': 'atx',
});
} else {
console.error("找不到 TurndownService,无法处理复杂Markdown排版。\n请确认 " + TurndownService + " 是否正常加载。");
}
// 加载 github flavored markdown 插件
if (turndownService && typeof turndownPluginGfm == 'object') {
turndownService.use(turndownPluginGfm.tables);
turndownService.use(turndownPluginGfm.taskListItems);
// 删除线
// turndownPluginGfm.strikethrough 实现的不正确,虎绿林只支持 ~~删除线~~,不支持 ~删除线~
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: function (content) {
return '~~' + content + '~~';
}
});
// 代码高亮
turndownService.addRule('highlightedCodeBlock', {
filter: function (node) {
return node.nodeName === 'PRE' && node.querySelector('div.code-wrapper');
},
replacement: function (content, node, options) {
console.log(content, node, options);
var lang = node.querySelector('span.code-lang')?.textContent || ''; // lang span可能不存在
var code = Array.from(node.querySelectorAll('div.code-wrapper td.hljs-ln-code')).map(x => x.textContent).join("\n");
var fence = (() => {
switch (replyCodeFormat) {
case 'html':
return ['[html' + replyCodeFormatOpts + ']', '[/html]'];
case 'text':
return ['[text]', '[/text]'];
case 'math':
return ['[math]', '[/math]'];
case 'latex':
return [options.fence + 'latex', options.fence];
default:
return [options.fence + lang, options.fence];
}
})();
return (
'\n\n' + fence[0] + '\n' +
code +
'\n' + fence[1] + '\n\n'
)
}
});
} else if (turndownService) {
console.error("找不到 turndownPluginGfm,无法处理复杂Markdown排版。\n请确认 " + turndownGfmJsUrl + " 是否正常加载。");
}
} catch (ex) {
console.error('turndown 加载失败', ex);
}
// 获取内容DOM
let reply = null;
// 等待内容出现
i = 0;
do {
reply = getLastReply();
i++;
} while (i<50 && !reply && !await sleep(100));
// 如果内容不为空,至少会有一个Text子节点
if (!reply || !reply.childNodes || reply === lastReply) {
if (isNewSession && isTextEmpty) {
return "会话不存在,无法读取上一条回复。请发送非空留言。";
}
return translateErrorMessage(await autoRetry("READ_REPLY_FAILED"));
}
let errorMessage = '';
if (errorMap[reply.textContent.substr(0, errorMaxLen)]) {
errorMessage = translateErrorMessage(await autoRetry(reply.textContent));
// 获取部分回复
reply = getLastReply(1);
if (!reply || !reply.childNodes || reply === lastReply) {
// 没有部分回复,直接返回错误信息
return errorMessage;
}
errorMessage = "\n\n----------\n\n" + errorMessage;
}
// 用户要求原始回复,或内容包含数学公式,直接回复HTML代码
if (replyCodeFormat == 'raw' || reply.querySelector('math,mjx-container')) {
return `[html${replyCodeFormatOpts}]
<!doctype html>
<head>
<link rel="stylesheet" href="https://hu60.cn/tpl/jhin/css/default.css"/>
<link rel="stylesheet" href="https://hu60.cn/tpl/jhin/css/new.css"/>
<link rel="stylesheet" href="https://hu60.cn/tpl/jhin/css/github-markdown.css"/>
<link rel="stylesheet" href="https://hu60.cn/tpl/jhin/js/katex/dist/katex.min.css">
</head>
<body style="background-color: white">
<div class="markdown-body">
${reply.innerHTML}
</div>
<div class="error-message">${errorMessage}</div>
</body>
[/html]`;
}
// 用插件 html 转 markdown
if (turndownService) {
try {
return turndownService.turndown(reply) + errorMessage;
} catch (ex) {
console.error('turndown 转换失败', ex);
}
}
// 插件加载或转换失败,手动 html 转 markdown
let lines = [];
reply.childNodes.forEach(x => {
if (x.tagName == 'PRE') { // 代码
let lang = x.querySelector('span')?.innerText || '';
let code = x.querySelector('code').innerText.replace(/[\r\n]+$/s, '');
lines.push("\n```" + lang + "\n" + code + "\n```\n");
} else { // 正文
lines.push(x.innerText);
}
});
return lines.join("\n\n") + errorMessage;
}
// 判断响应是否结束
function isFinished() {
let stopOrRegenButton = document.querySelector(stopOrRegenButtonSelector);
let waitButton = document.querySelector(replyNotReadySelector);
if (stopOrRegenButton && stopOrRegenButton.textContent == '停止生成') {
return false;
}
if (waitButton && waitButton.style.display != "none") {
return false;
}
return true;
}
// 自动重试
async function autoRetry(errorMessage) {
if (!localStorage.lastAtInfo) {
return errorMessage;
}
let atInfo = JSON.parse(localStorage.lastAtInfo);
atInfo.retryTimes = atInfo.retryTimes || 0;
if (errorMessage != '网络错误' && atInfo.retryTimes < 5) {
console.warn('自动重试', errorMessage);
refreshPage();
await sleep(5000);
}
return errorMessage;
}
// 读取@消息
async function readAtInfo() {
// 读取保存的进度
if (localStorage.lastAtInfo) {
try {
let atInfo = JSON.parse(localStorage.lastAtInfo);
atInfo.retryTimes = atInfo.retryTimes || 0;
if (atInfo.retryTimes < 5) {
atInfo.retryTimes++;
console.log('载入上次的@消息', atInfo);
return atInfo;
}
} catch (ex) {
console.log('读取保存的@消息出错', ex);
}
}
let response = await fetch(hu60BaseUrl + 'msg.index.@.no.json?_origin=*&_json=compact&_content=json&_time=1', {
redirect: "manual" // 不自动重定向
});
if (response.type == 'opaqueredirect') {
// 登录失效,要求重新登录
await login(true);
return await readAtInfo();
}
return await response.json();
}