分类: 未分类
20260223基于zot registry搭建本地镜像仓库
镜像获取
直接下载
|
0 1 2 |
docker pull ghcr.io/project-zot/zot:v2.1.14 |
百度云
上传过程记录
|
0 1 2 3 |
docker load -i zot-v2.1.14.tar.gz docker load -i zot-v2.1.14-qunhui.tar.gz docker image ls |
从百度云下载
百度云总文件夹: https://pan.baidu.com/s/5U9ZYswxhKohgcmqKvdopPg
通用镜像: https://pan.baidu.com/s/1xYEncWT2WIVNL23bPGA8rw?pwd=py3z
群晖镜像: https://pan.baidu.com/s/1JuQqTgrf9fE46rHpTiWAuw?pwd=hmm2
|
0 1 2 3 |
docker load -i zot-v2.1.14.tar.gz docker load -i zot-v2.1.14-qunhui.tar.gz docker image ls |
准备相关文件夹和配置文件
|
0 1 2 3 4 5 6 7 8 |
BASE_DIR=/volume1/docker/zot mkdir -p $BASE_DIR/etc mkdir -p $BASE_DIR/data mkdir -p $BASE_DIR/var/log touch $BASE_DIR/etc/config.json touch $BASE_DIR/etc/htpasswd touch $BASE_DIR/var/log/zot.log touch $BASE_DIR/var/log/zot-audit.log |
密码文件$BASE_DIR/etc/htpasswd随便找个服务器生成
|
0 1 2 3 |
apt install apache2-utils htpasswd -bnB hyl hyltest > $BASE_DIR/etc/htpasswd echo 'hyl:$2y$05$0IClVO/Yg0LBeWenhf.EzuNK/23oTjBeViAIbjE6SlPi07Dz21LOW' |
配置文件$BASE_DIR/etc/config.json内容如下
|
0 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 |
{ "distSpecVersion": "1.0.1", "storage": { "dedupe": true, "gc": true, "gcDelay": "1h", "gcInterval": "6h", "rootDirectory": "/data/zot" }, "http": { "address": "0.0.0.0", "externalUrl": "http://registry.hylstudio.local", "port": "80", "compat": [ "docker2s2" ], "auth": { "htpasswd": { "path": "/etc/zot/htpasswd" } }, "accessControl": { "adminPolicy": { "users": [ "hyl" ], "actions": [ "read", "create", "update", "delete" ] }, "repositories": { "**": { "anonymousPolicy": [ "read", "create", "update" ] } } } }, "log": { "level": "debug", "output": "/var/log/zot/zot.log", "audit": "/var/log/zot/zot-audit.log" }, "extensions": { "ui": { "enable": true }, "search": { "enable": true, "cve": { "updateInterval": "24h" } }, "sync": { "enable": true, "registries": [ { "content": [ { "destination": "/quay.io", "prefix": "**" } ], "onDemand": true, "tlsVerify": true, "urls": [ "https://quay.io" ] }, { "content": [ { "destination": "/xpkg.upbound.io", "prefix": "**" } ], "onDemand": true, "tlsVerify": true, "urls": [ "https://xpkg.upbound.io" ] }, { "content": [ { "prefix": "**" } ], "onDemand": true, "tlsVerify": true, "urls": [ "https://registry-1.docker.io" ] }, { "content": { "destination": "/registry.k8s.io", "prefix": "**" }, "onDemand": true, "tlsVerify": true, "urls": [ "https://registry.k8s.io" ] }, { "content": { "destination": "/gcr.io", "prefix": "**" }, "onDemand": true, "tlsVerify": true, "urls": [ "https://gcr.io" ] }, { "content": { "destination": "/ghcr.io", "prefix": "**" }, "onDemand": true, "tlsVerify": true, "urls": [ "https://ghcr.io" ] }, { "content": { "destination": "/registry.gitlab.com", "prefix": "**" }, "onDemand": true, "tlsVerify": true, "urls": [ "https://registry.gitlab.com" ] } ] } } } |
启动服务
通用镜像
|
0 1 2 3 4 5 6 7 8 9 |
docker rm -f zot docker run -d --name zot \ -p 8080:80 \ -v $BASE_DIR/etc/htpasswd:/etc/zot/htpasswd \ -v $BASE_DIR/etc/config.json:/etc/zot/config.json \ -v $BASE_DIR/var/log/zot.log:/var/log/zot/zot.log \ -v $BASE_DIR/var/log/zot-audit.log:/var/log/zot/zot-audit.log \ -v $BASE_DIR/data:/data/zot \ ghcr.io/project-zot/zot:v2.1.14 |
访问
|
0 1 2 |
http://192.168.0.3:8080 |
群晖镜像
制作过程记录
群晖Container Manager需要镜像主动声明EXPOSE端口才能勾选web stationd配置,手动修改镜像如下
|
0 1 2 |
FROM ghcr.io/project-zot/zot:v2.1.14 EXPOSE 80 |
打包到本地文件上传到百度云
|
0 1 2 3 4 |
docker tag -t registry.hylstudio.local/ghcr.io/project-zot/zot:v2.1.14-expose . docker push registry.hylstudio.local/ghcr.io/project-zot/zot:v2.1.14-expose docker tag registry.hylstudio.local/ghcr.io/project-zot/zot:v2.1.14-expose ghcr.io/project-zot/zot:v2.1.14-expose docker save -o zot-v2.1.14-qunhui.tar.gz ghcr.io/project-zot/zot:v2.1.14-expose |
启动命令
|
0 1 2 3 4 5 6 7 |
docker run -d --name zot \ -v $BASE_DIR/etc/htpasswd:/etc/zot/htpasswd \ -v $BASE_DIR/etc/config.json:/etc/zot/config.json \ -v $BASE_DIR/var/log/zot.log:/var/log/zot/zot.log \ -v $BASE_DIR/var/log/zot-audit.log:/var/log/zot/zot-audit.log \ -v $BASE_DIR/data:/data/zot \ ghcr.io/project-zot/zot:v2.1.14-expose |
手动启动一次后,通过群晖网页修改后续配置暴露即可
访问
|
0 1 2 |
http://registry.hylstudio.local |
参考文档
https://github.com/project-zot/zot
https://zotregistry.dev/v2.1.14/install-guides/install-guide-linux/#about-binary-images
https://zotregistry.dev/v2.1.14/articles/mirroring/#mirroring-modes
https://zotregistry.dev/v2.1.14/general/releases/?h=docker#getting-container-images
20250830算法题解析-偏爱的字符串
废话
最近博客突然打不开了,看了一眼是PHP开始报莫名奇妙的错误。这个博客主题是当年随便从网上找的一个,历经多个PHP和wordpress大版本变化还凑合能正常展示已经非常不易了,中途历经多次魔改和打补丁。原计划是在10周年之前把所有文章彻底重写一次换掉wordpress,但以我这个鸽子的风格看了一眼还有1个月时间显然不现实,所以这次我直接放弃了,直接用官方的模板改了改css就成了现在这个样子。
看了一眼历史记录,我的第一篇博客还是在大学时候联系ubuntu linux搭建wodpress后写的,当时正好是大一的期末即将C语言考试,写了一系列的C语言教程当作期末复习。众所周知我的算法比较弱(相比于栋神或然妞),并且从未在算法上怎么努力过,就这么得过且过了10年,做深度算法的分析屈指可数。印象中比较清晰的只有KMP算法、Levenshtein距离、上班时为了优化聊天室pin消息new红点的算法、为了模仿outlook日程布局设计的排版算法。
前段时间在群里看到有人在讨论一个算法题,我简单分享了思路,但懒得动手。这次正好做个深度解析。顺带一提虽然现在ai编程非常的火,工作的话因为讲究效率优先有时候无需考虑原理,只要结果对就行。但长期来看我的态度还是要磨练下基本功,不然AI错了你根本纠正不回来,所以本次不会使用任何AI补全,完全一步步手动实现再逐步优化。
原题目
https://www.nowcoder.com/practice/294da7ea5f184655a31bf659035fc874?tab=note
视频版
https://www.bilibili.com/video/BV1cChqzqEh8
描述
假定小李有 m 个偏爱的字符,依次为c1,c2…cm,当小李看到一个长度为 n 的字符串 s 时,请你输出小李在进行全部替换操作后形成的字符串。
输入描述:
接下来一行输入m个字符c1,c2…cm,每两个字符之间用空格隔开,表示小李偏爱的字符。
1≤n≤1e5,1≤m≤26,保证题目中所有的字符均为大写字符,小李偏爱的字符互不相同,且偏爱字符至少出现一次。
输出描述:
输入:
|
0 1 2 3 |
12 4 Z G B A ZQWEGRTBYAAI |
输出:
|
0 1 |
ZZZGGGBBBAAA |
说明:
|
0 1 2 3 4 5 |
字符Q为非偏爱字符,且偏爱字符Z距离它最近,所以替换成Z;同理E距离G最近,替换成G; 对于字符W,偏爱字符Z和G与其距离相同,所以替换为左边的Z; ....... 对于字符 I ,右边没有偏爱字符,左边第一个偏爱字符是A,所以替换成字符A。 同一个偏爱字符可能会在字符串中出现多次。 |
题目分析
我做算法题因为没有限时,所以一般来说我都是先用暴力手段写正确结果,再逐步优化
题目里废话实在太多,翻译成人话就是把后续所有输入都改成指定的几个字母,以最近的为准,如果距离一样用左边的。
暴力版本
|
0 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 |
import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collector; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { //输入: //12 4 //Z G B A //ZQWEGRTBYAAI String inputStr = "ZQWEGRTBYAAI"; List<Character> inputStrChars = convert(inputStr); Set<Character> preferChars = new HashSet<>(4); preferChars.add('Z'); preferChars.add('G'); preferChars.add('B'); preferChars.add('A'); // System.out.println(inputStr); System.out.println(inputStrChars); // System.out.println(preferChars); String result = replaceChars(inputStrChars, preferChars); // String result = replaceChars1(inputStrChars, preferChars); // String result = replaceChars2(inputStrChars, preferChars); // String result = replaceChars3(inputStrChars, preferChars); System.out.println(result); //输出: //ZZZGGGBBBAAA } private static String replaceChars(List<Character> inputStrChars, Set<Character> preferChars) { List<Character> result = new ArrayList<>(inputStrChars.size()); // return "ZZZGGGBBBAAA"; // inputStrChars = convert("ZZZGGGBBBAAA"); for (int i = 0; i < inputStrChars.size(); i++) { Character currentChar = inputStrChars.get(i); if (preferChars.contains(currentChar)) { result.add(currentChar); } else { //find first preferChars in left int currentIndex = i; int leftDistance = findLeftPreferCharDistance(inputStrChars, currentIndex,preferChars); Character leftPreferChar = inputStrChars.get(currentIndex - leftDistance); //find first preferChars in right int rightDistance = findRightPreferCharDistance(inputStrChars, currentIndex,preferChars); Character rightPreferChar = inputStrChars.get(currentIndex + rightDistance); //compare distance Character replacedChar = 'A'; if (leftDistance <= rightDistance) { replacedChar = leftPreferChar; } else { replacedChar = rightPreferChar; } result.add(replacedChar); } } return convert(result); } private static int findLeftPreferCharDistance(List<Character> inputStrChars, int currentIndex, Set<Character> preferChars) { for (int i = currentIndex; i >=0; i--) { Character currentChar = inputStrChars.get(i); if (preferChars.contains(currentChar)) { return Math.abs(currentIndex - i); } } System.out.printf("find left prefer char error, currentIndex = %s\n", currentIndex); return -1; } private static int findRightPreferCharDistance(List<Character> inputStrChars, int currentIndex, Set<Character> preferChars) { for (int i = currentIndex; i < inputStrChars.size(); i++) { Character currentChar = inputStrChars.get(i); if (preferChars.contains(currentChar)) { return Math.abs(currentIndex - i); } } System.out.printf("find right prefer char error, currentIndex = %s\n", currentIndex); return -1; } private static String convert(List<Character> result) { String resultStr = result.stream().map(v -> v + "").collect(Collectors.joining("")); return resultStr; } private static List<Character> convert(String inputStr) { List<Character> result = new ArrayList<>(inputStr.length()); for (int i = 0; i < inputStr.length(); i++) { result.add(inputStr.charAt(i)); } return result; } } |
加一点优化
根据刚才的运行结果,如果尝试打印寻找过程
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//0 find left prefer char, currentIndex = 1, distance = 1 find right prefer char, currentIndex = 1, distance = 3 find left prefer char, currentIndex = 2, distance = 2 find right prefer char, currentIndex = 2, distance = 2 find left prefer char, currentIndex = 3, distance = 3 find right prefer char, currentIndex = 3, distance = 1 //4 find left prefer char, currentIndex = 5, distance = 1 find right prefer char, currentIndex = 5, distance = 2 find left prefer char, currentIndex = 6, distance = 2 find right prefer char, currentIndex = 6, distance = 1 //7 find left prefer char, currentIndex = 8, distance = 1 find right prefer char, currentIndex = 8, distance = 1 //9 //10 find left prefer char, currentIndex = 11, distance = 1 find right prefer char error, currentIndex = 11 |
注意到每次左侧寻找的距离都是递增的,so优化思路就是不要每次都从头开始搜,如果还没出现新的偏爱字符,那上一次寻找的结果直接+1就是本次搜索的结果,如果距离右侧最新的偏爱字符过半了,那就是右侧的结果。因此之前循环的状态需要记录下左侧和右侧的距离或索引。
TODO 待编写代码
再加一点优化
按上面的思路,如果能提前知道哪些位置是偏爱字符,那么根本不需要逐个字符判断、搜索、替换,可以直接按偏爱字符的位置就能计算最终的结果。你会发现最终结果只和偏爱字符的位置有关,和其他输入的字符无关,你甚至都不需要读入其他字符也能计算出最终结果
TODO 待编写代码
另类思路
按上面所说既然结果和其他字符无关,只需要特殊字符和对应的位置+总字符串长度就能计算的话。实时处理也是可能的,这也是目前AI输入输出常见的表现形式,打字机效果。当输入一个字符后,实时输出最终结果,当发现前几个字符结果不对了,删除前n个字符后纠正回正确的字符。这种算法不需要预处理,但因为无法预知后续输入所以必须有纠正的过程出现。实际实现逻辑只需记录最后一次输入的偏爱字符和当前字符和最后一次输入的偏爱字符距离,即可知道是应该打印最后一次的偏爱字符,还是应该删除前n个距离当前输入的偏爱字符更近的字符。
TODO 待编写代码
20250503jupyter-mcp-server自动化
背景
在我尝试将jupyter kotlin kernel 内嵌到IDEA的JVM后,用jupyter控制IDEA用了很长时间,但除了utils工具类之外IDEA的openapi和公开的方法基本都是为了UI而编写的异步代码,这在自动化过程中获取返回值非常不方便。我预期的是直接复制粘贴官方源码稍微改改就能用,不想对官方代码做大的结构修改。
继IDEA官方编写MCP后,我发现二月份的时候jupyter也有MCP server了。
datalayer在尝试用ai把jupyter自动化,使用自然语言描述数据处理的请求然后生成notebook并执行python代码,我一看这不正好是我想要的么。
如果我人工能操作jupyterlab完成结果,相比调整kotlin脚本的控制流,我只需要糊一点python代码就能代替我手工操作的部分
原理
jupyter mcp server的代码非常的少,我想要的其实是排除mcp的部分,去掉注解改一改直接就能用
本质上是直接调用了jupyter lab的接口获取websocket,通过jupyter lab的collaboration扩展通道操作notebook
过程记录
安装jupyter lab,注意这里新增了jupyter-collaboration
|
0 1 |
python -m pip install jupyterlab kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
实测发现之前的安装命令不好使了,日志报错如下
|
0 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 |
2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - 28669 [Thread-90] DEBUG SocketWrapper - [SHELL] >rcv: msg[0b76731b-0290-4aec-ad16-e36095f16608] {"header":{"date":"2025-05-02T12:03:36.600Z","msg_id":"6000f0b3-e290-46b0-a388-7b960e068dfb","msg_type":"kernel_info_request","session":"0b76731b-0290-4aec-ad16-e36095f16608","username":"","subshell_id":null,"version":"5.2"},"parent_header":null,"metadata":null,"content":{}} 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - 28669 [Thread-90] ERROR SocketWrapper - Exception thrown while processing a message 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - kotlinx.serialization.json.internal.JsonDecodingException: Encountered an unknown key 'subshell_id'. 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys. 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - Current input: .....095f16608","username":"","subshell_id":null,"version":"5.2"} 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonExceptionsKt.UnknownKeyException(JsonExceptions.kt:71) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.JsonTreeDecoder.endStructure(TreeJsonDecoder.kt:276) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageHeader$$serializer.deserialize(message_types.kt:111) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageHeader$$serializer.deserialize(message_types.kt:111) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.internal.NullableSerializer.deserialize(NullableSerializer.kt:30) 2025-05-02 20:03:36,616 [ 31112] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:117) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageDataSerializer.deserialize(message_types.kt:603) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageDataSerializer.deserialize(message_types.kt:537) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:117) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.MessageKt.toMessage(message.kt:115) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.messaging.ProtocolKt.shellMessagesHandler(protocol.kt:243) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$2.invoke(KotlinReplWrapper.kt:220) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$2.invoke(KotlinReplWrapper.kt:218) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.AbstractJupyterConnection$addMessageCallback$socketCallback$1.invoke(AbstractJupyterConnection.kt:18) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.AbstractJupyterConnection$addMessageCallback$socketCallback$1.invoke(AbstractJupyterConnection.kt:16) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at org.jetbrains.kotlinx.jupyter.protocol.SocketWrapper.runCallbacksOnMessage(SocketWrapper.kt:62) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$3.invoke(KotlinReplWrapper.kt:237) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper$kernelServer$1$3.invoke(KotlinReplWrapper.kt:236) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer$lambda$6$socketLoop(KotlinReplWrapper.kt:202) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer(KotlinReplWrapper.kt:236) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.kernelServer$default(KotlinReplWrapper.kt:167) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.embedKernelIdea(KotlinReplWrapper.kt:164) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.embedKernelIdea$default(KotlinReplWrapper.kt:145) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.main(KotlinReplWrapper.kt:76) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.KotlinReplWrapper.makeEmbeddedRepl(KotlinReplWrapper.kt:54) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at cn.hylstudio.skykoma.plugin.idea.service.impl.IdeaPluginAgentServerImpl.lambda$startJupyterKernel$2(IdeaPluginAgentServerImpl.java:281) 2025-05-02 20:03:36,617 [ 31113] INFO - STDERR - at java.base/java.lang.Thread.run(Thread.java:833) |
很明显kernel接到了jupyterlab的请求但反序列化失败了,注意到jupyterlab没指定版本
根据上一次安装记录对比版本号可发现,是最近jupyterlab的更新4.4.0时header新增了一个字段,而kotlin kernel反序列化不接受未定义的字段导致,因此降级到4.3的最后一个版本4.3.6
|
0 1 |
python -m pip install jupyterlab==4.3.6 kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
|
0 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 |
------------------------- -------------- anyio 4.9.0 argon2-cffi 23.1.0 argon2-cffi-bindings 21.2.0 arrow 1.3.0 asttokens 3.0.0 async-lru 2.0.5 attrs 25.3.0 babel 2.17.0 beautifulsoup4 4.13.4 bleach 6.2.0 certifi 2025.4.26 cffi 1.17.1 charset-normalizer 3.4.2 colorama 0.4.6 comm 0.2.2 datalayer_pycrdt 0.12.15 debugpy 1.8.14 decorator 5.2.1 defusedxml 0.7.1 executing 2.2.0 fastjsonschema 2.21.1 fqdn 1.5.1 h11 0.16.0 httpcore 1.0.9 httpx 0.28.1 idna 3.10 ipykernel 6.29.5 ipython 9.2.0 ipython_pygments_lexers 1.1.1 isoduration 20.11.0 jedi 0.19.2 Jinja2 3.1.6 json5 0.12.0 jsonpointer 3.0.0 jsonschema 4.23.0 jsonschema-specifications 2025.4.1 jupyter_client 8.4.3 jupyter_core 5.7.2 jupyter_kernel_client 0.6.0 jupyter_nbmodel_client 0.11.3 jupyter_server 2.15.0 jupyter_server_fileid 0.9.3 jupyter_server_terminals 0.5.3 jupyter-collaboration 3.1.2 jupyter-collaboration-ui 1.1.2 jupyter-docprovider 1.1.2 jupyter-events 0.12.0 jupyter-lsp 2.2.5 jupyter-server-ydoc 1.1.2 jupyter-ydoc 3.0.4 jupyterlab 4.3.6 jupyterlab_pygments 0.3.0 jupyterlab_server 2.27.3 jupyterlab-lsp 5.1.0 kotlin-jupyter-kernel 0.12.0.322 MarkupSafe 3.0.2 matplotlib-inline 0.1.7 mistune 3.1.3 nbclient 0.10.2 nbconvert 7.16.6 nbformat 5.10.4 nest-asyncio 1.6.0 notebook_shim 0.2.4 overrides 7.7.0 Package Version packaging 25.0 pandocfilters 1.5.1 parso 0.8.4 pip 25.0.1 platformdirs 4.3.7 prometheus_client 0.21.1 prompt_toolkit 3.0.51 psutil 7.0.0 pure_eval 0.2.3 pycparser 2.22 pycrdt 0.12.15 pycrdt-websocket 0.15.5 Pygments 2.19.1 python-dateutil 2.9.0.post0 python-json-logger 3.3.0 pywin32 310 pywinpty 2.0.15 PyYAML 6.0.2 pyzmq 26.4.0 referencing 0.36.2 requests 2.32.3 rfc3339-validator 0.1.4 rfc3986-validator 0.1.1 rpds-py 0.24.0 run_kotlin_kernel_idea 0.1 Send2Trash 1.8.3 setuptools 80.2.0 six 1.17.0 sniffio 1.3.1 soupsieve 2.7 sqlite-anyio 0.2.3 stack-data 0.6.3 terminado 0.18.1 tinycss2 1.4.0 tornado 6.4.2 traitlets 5.14.3 types-python-dateutil 2.9.0.20241206 typing_extensions 4.13.2 uri-template 1.3.0 urllib3 2.4.0 wcwidth 0.2.13 webcolors 24.11.1 webencodings 0.5.1 websocket-client 1.8.0 websockets 15.0.1 |
降级后测试原来的功能正常
第一次先使用再建一个独立virtualenv防止破坏环境
|
0 1 |
pip install jupyter-kernel-client==0.6.0 jupyter-nbmodel-client==0.11.3 |
按官方文档安装依赖,修改官方的源码作为测试脚本。因为官方是使用独立进程的python kernel运行脚本,而我是要让一个已经运行了的内嵌kernel执行,所以需要指定下当前的kernel_id复用。否则代码会启动一个新的python kernel执行kotlin脚本就和我的预期不符了
|
0 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 |
import logging import os from jupyter_kernel_client import KernelClient from jupyter_nbmodel_client import ( NbModelClient, get_jupyter_notebook_websocket_url, ) logger = logging.getLogger(__name__) def extract_output(output: dict) -> str: """ Extracts readable output from a Jupyter cell output dictionary. Args: output (dict): The output dictionary from a Jupyter cell. Returns: str: A string representation of the output. """ output_type = output.get("output_type") if output_type == "stream": return output.get("text", "") elif output_type in ["display_data", "execute_result"]: data = output.get("data", {}) if "text/plain" in data: return data["text/plain"] elif "text/html" in data: return "[HTML Output]" elif "image/png" in data: return "[Image Output (PNG)]" else: return f"[{output_type} Data: keys={list(data.keys())}]" elif output_type == "error": return output["traceback"] else: return f"[Unknown output type: {output_type}]" async def add_markdown_cell(cell_content: str) -> str: """Add a markdown cell in a Jupyter notebook. Args: cell_content: Markdown content Returns: str: Success message """ notebook = NbModelClient( get_jupyter_notebook_websocket_url(server_url=SERVER_URL, token=TOKEN, path=NOTEBOOK_PATH) ) await notebook.start() notebook.add_markdown_cell(cell_content) await notebook.stop() return "Jupyter Markdown cell added." async def add_execute_code_cell(cell_content: str, kernel) -> list[str]: """Add and execute a code cell in a Jupyter notebook. Args: cell_content: Code content Returns: list[str]: List of outputs from the executed cell """ notebook = NbModelClient( get_jupyter_notebook_websocket_url(server_url=SERVER_URL, token=TOKEN, path=NOTEBOOK_PATH) ) await notebook.start() cell_index = notebook.add_code_cell(cell_content) if kernel: notebook.execute_cell(cell_index, kernel) ydoc = notebook._doc outputs = ydoc._ycells[cell_index]["outputs"] str_outputs = [extract_output(output) for output in outputs] await notebook.stop() return str_outputs def get_sessions(): import requests # 获取当前Notebook的会话信息 response = requests.get( f"{SERVER_URL}/api/sessions", headers={"Authorization": f"token {TOKEN}"} ) sessions = response.json() # [{"id": "d00ac394-cace-42c1-b56d-a93e7ff9d3a4", "path": "Untitled.ipynb", "name": "Untitled.ipynb", "type": "notebook", "kernel": {"id": "9a1ae062-ed29-49af-862e-c455109283f3", "name": "kotlin_skykoma-agent-idea", "last_activity": "2025-05-02T13:26:07.157812Z", "execution_state": "idle", "connections": 1}, "notebook": {"path": "Untitled.ipynb", "name": "Untitled.ipynb"}}] return sessions def get_kernel_id(): # 提取目标Notebook的Kernel连接信息 kernel_info = None sessions = get_sessions() for session in sessions: if session['notebook']['path'] == NOTEBOOK_PATH: kernel_info = session['kernel'] break # print("Kernel Connection Info:", kernel_info) kernel_id = None if kernel_info: kernel_id = kernel_info["id"] # kernel_name="kotlin_skykoma-agent-idea if kernel_id is None: print(f"get kernel_id error, sessions = {sessions}") return kernel_id NOTEBOOK_PATH = "test/Untitled1.ipynb" SERVER_URL = "http://127.0.0.1:8888" TOKEN = "1234" async def main(code): url = f"{SERVER_URL}/api/sessions" result = await add_markdown_cell("test") print(f"markdown add result = {result}") kernel_id = get_kernel_id() if(kernel_id == None): exit(-1) kernel = KernelClient(server_url=SERVER_URL, token=TOKEN, kernel_id=kernel_id) kernel.start() result = await add_execute_code_cell(code, kernel) kernel.stop() print(f"code execute result = {result}") import asyncio if __name__ == "__main__": code = """ import cn.hylstudio.skykoma.plugin.idea.util.* println("succ") """.lstrip() asyncio.run(main(code)) exit(0) |
既然python文件单独执行是能成功的,也就意味着python kernel也能执行相同的代码,而一个jupyterlab可以同时开启notebook分配不同的kernel。既然如此已经有一个现成的python环境了为什么不直接用呢?因此续尝试合并这个独立的python环境到jupyterlab的python环境
|
0 1 2 3 4 |
datalayer_pycrdt 0.12.15 jupyter_kernel_client 0.6.0 jupyter_nbmodel_client 0.11.3 websockets 15.0.1 |
合并后的安装命令
|
0 1 |
python -m pip install jupyterlab==4.3.6 kotlin-jupyter-kernel jupyterlab-lsp jupyter-collaboration jupyter-kernel-client==0.6.0 jupyter-nbmodel-client==0.11.3 git+https://github.com/956237586/run_kotlin_kernel_idea.git@v0.1 git+https://github.com/956237586/jupyter_client.git@v8.4.3 |
然后就可以尝试将脚本放入另一个notebook中了,kernel如下
执行效果如下
后续计划
参考
https://github.com/dev-assistant/skykoma-plugin-idea
https://github.com/datalayer/jupyter-mcp-server/tree/main/jupyter_mcp_server
20250323postwoman/hoppscotch私有化部署
背景
尝试搭建个私有化的postman玩
视频版见B站 https://www.bilibili.com/video/BV1SDoBYrEjC
准备
一台虚拟机
一个域名
一个SMTP服务器:我用的是qq的,需要一个额外授权码当密码用
安装docker和docker-compose
略
准备环境配置
注:安全起见SMTP密码在视频发布后会失效
|
0 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 |
#-----------------------Backend Config------------------------------# # Prisma Config DATABASE_URL=postgresql://postgres:6fyVResuychxGH8e@pgsql-hoppscotch:5432/hoppscotch?connect_timeout=300 # or replace with your database URL # (Optional) By default, the AIO container (when in subpath access mode) exposes the endpoint on port 80. Use this setting to specify a different port if needed. HOPP_AIO_ALTERNATE_PORT=80 # Auth Tokens Config JWT_SECRET=hVhdksivV6VNkF0H5TwSqJwyaV5llwO2 TOKEN_SALT_COMPLEXITY=10 MAGIC_LINK_TOKEN_VALIDITY=3 # Default validity is 7 days (604800000 ms) in ms REFRESH_TOKEN_VALIDITY=604800000 # Default validity is 1 day (86400000 ms) in ms ACCESS_TOKEN_VALIDITY=86400000 SESSION_SECRET=MaQSjhcPlUTNsxjDNbJrggkpirL2OHiq # Recommended to be true, set to false if you are using http # Note: Some auth providers may not support http requests ALLOW_SECURE_COOKIES=false # Sensitive Data Encryption Key while storing in Database (32 character) DATA_ENCRYPTION_KEY=wmaVSie5FpRj89yTlHx70jQN7iIZ7M4F # Hoppscotch App Domain Config REDIRECT_URL=http://postwoman.hylstudio.local # Whitelisted origins for the Hoppscotch App. # This list controls which origins can interact with the app through cross-origin comms. # - localhost ports (3170, 3000, 3100): app, backend, development servers and services # - app://localhost_3200: Bundle server origin identifier # NOTE: `3200` here refers to the bundle server (port 3200) that provides the bundles, # NOT where the app runs. The app itself uses the `app://` protocol with dynamic # bundle names like `app://{bundle-name}/` WHITELISTED_ORIGINS=http://postwoman.hylstudio.local:3170,http://postwoman.hylstudio.local:3000,http://postwoman.hylstudio.local:3100,app://localhost_3200,app://hoppscotch # VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL VITE_ALLOWED_AUTH_PROVIDERS=EMAIL # Google Auth Config GOOGLE_CLIENT_ID=************************************************ GOOGLE_CLIENT_SECRET=************************************************ GOOGLE_CALLBACK_URL=http://localhost:3170/backend/v1/auth/google/callback GOOGLE_SCOPE=email,profile # Github Auth Config GITHUB_CLIENT_ID=************************************************ GITHUB_CLIENT_SECRET=************************************************ GITHUB_CALLBACK_URL=http://localhost:3170/backend/v1/auth/github/callback GITHUB_SCOPE=user:email # Microsoft Auth Config MICROSOFT_CLIENT_ID=************************************************ MICROSOFT_CLIENT_SECRET=************************************************ MICROSOFT_CALLBACK_URL=http://localhost:3170/backend/v1/auth/microsoft/callback MICROSOFT_SCOPE=user.read MICROSOFT_TENANT=common # Mailer config MAILER_SMTP_ENABLE=true MAILER_USE_CUSTOM_CONFIGS=false MAILER_ADDRESS_FROM=master@hylstudio.cn MAILER_SMTP_URL=smtps://956237586:mvzippsgybukbfgi@smtp.qq.com # used if custom mailer configs is false # The following are used if custom mailer configs is true MAILER_SMTP_HOST=smtp.domain.com MAILER_SMTP_PORT=587 MAILER_SMTP_SECURE=true MAILER_SMTP_USER=user@domain.com MAILER_SMTP_PASSWORD=pass MAILER_TLS_REJECT_UNAUTHORIZED=true # Rate Limit Config RATE_LIMIT_TTL=60 # In seconds RATE_LIMIT_MAX=100 # Max requests per IP #-----------------------Frontend Config------------------------------# # Base URLs VITE_BASE_URL=http://postwoman.hylstudio.local VITE_SHORTCODE_BASE_URL=http://postwoman.hylstudio.local/share VITE_ADMIN_URL=http://postwoman.hylstudio.local/admin # Backend URLs VITE_BACKEND_GQL_URL=http://postwoman.hylstudio.local/backend/graphql VITE_BACKEND_WS_URL=wss://postwoman.hylstudio.local/backend/graphql VITE_BACKEND_API_URL=http://postwoman.hylstudio.local/backend/v1 # Terms Of Service And Privacy Policy Links (Optional) VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy # Set to `true` for subpath based access ENABLE_SUBPATH_BASED_ACCESS=true |
准备compose文件
|
0 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 |
services: hoppscotch: image: registry.hylstudio.local/hoppscotch/hoppscotch container_name: hoppscotch env_file: - ./.env restart: always depends_on: - pgsql-hoppscotch ports: - "80:80" pgsql-hoppscotch: image: registry.hylstudio.local/postgres:17 container_name: pgsql-hoppscotch # This will be exposed at port 5432 ports: - "5432:5432" environment: # NOTE: Please UPDATE THIS PASSWORD! POSTGRES_DB: hoppscotch POSTGRES_USER: postgres POSTGRES_PASSWORD: 6fyVResuychxGH8e PGDATA: /var/lib/postgresql/data/pgdata volumes: - ./data:/var/lib/postgresql/data |
首次启动
|
0 1 |
docker compose up -d |
注意首次需要初始化一下数据库
|
0 1 2 |
docker compose run --entrypoint sh hoppscotch pnpm dlx prisma migrate deploy |
进入后台并试玩
todo
参考
https://docs.hoppscotch.io/documentation/self-host/getting-started
https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build
20250201AI应用相关技术调研
背景
继上次调研后,很久没更新AI相关技术了,这次一并更新。
应用
全本地部署的ai https://github.com/mudler/LocalAI
应用框架
数据索引框架 data index framework
llamaindex
https://docs.llamaindex.ai/en/stable/ https://github.com/run-llama/llama_index
https://docs.llamaindex.ai/en/stable/use_cases/agents/ Property Graph Index可以用neo4j
https://docs.llamaindex.ai/en/stable/module_guides/storing
Vector Stores:Elasticsearch、Lantern、OpenSearch、Postgres、Qdrant
Document Stores:Mongo、Redis
Index Stores:Mongo、Redis
Chat Stores:Redis
Key-Value Stores:Mongo
Note: At the moment, these storage abstractions are not externally facing. https://docs.llamaindex.ai/en/stable/module_guides/storing/customization/
ai agents framework
自动化框架 restack
agents框架 eko
https://github.com/FellouAI/eko
pydantic
https://github.com/pydantic/pydantic-ai
composio
https://github.com/ComposioHQ/composio/tree/master/python/swe?utm_source=website
https://docs.composio.dev/framework/llamaindex
https://docs.composio.dev/framework/langchain
比langchain底层的工作流agent编排框架 LangGraph
prompt框架
https://github.com/langchain4j/langchain4j
https://docs.spring.io/spring-ai/reference/index.html
https://github.com/spring-projects/spring-ai
AI上下文协议框架MCP
https://modelcontextprotocol.io/introduction
https://modelcontextprotocol.io/quickstart
https://github.com/modelcontextprotocol
https://github.com/modelcontextprotocol/servers
知识库
上层
https://github.com/getzep/graphiti
向量数据库
https://lancedb.github.io/lancedb/concepts/storage/ embeddedable|localfile|s3
https://www.trychroma.com/ embeddedable|localfile
https://milvus.io/ need etcd、Pulsar embeddedable|s3
es dense_vector
20250131jupyter自动修改IDEA项目SDK、获取GIT信息
背景
在2023年4月的时候从phodal的项目中知道了jupyter core是可以embedded之后,我就尝试将它embedd进了IDEA自身以获取全量IDE的自动化能力
源码:https://github.com/dev-assistant/skykoma-plugin-idea
示例:https://github.com/dev-assistant/skykoma-plugin-idea/blob/main/demo/demo.ipynb
因部分java项目的sdk总是没法正确识别,IDEA多年以来的bug都需要手动选下无法完成全量的自动化,所以在官方处理之前想尝试解决下。部分kotlin简写本次一并更新,同时替换掉了基于命令行的git信息获取,改成git4idea接口和官方保持一致
20250201更新:
今天发现官方有了mcp-server的实现,以及已经有了基于MCP的客户端来集成但功能有限,我的插件甚至比MCP的出现(2024年11月25日)和IDEA官方MCP实现(2024年12月9日)要早得多(2023年4月)
|
0 1 2 3 4 5 6 7 8 9 |
官方插件 https://github.com/JetBrains/mcp-server-plugin https://plugins.jetbrains.com/plugin/26071-mcp-server 官方MCP server https://github.com/JetBrains/mcp-jetbrains goose集成 https://github.com/block/goose/blob/main/extensions-site/public/servers.json https://github.com/block/goose/tree/main/crates/goose-mcp/src/jetbrains |
自动设置SDK
注意到源码com.intellij.openapi.roots.ui.configuration.ProjectConfigurableUi就是project structure的实现
myProjectJdkConfigurable内容修改时候会触发
|
0 1 |
myLanguageLevelCombo.sdkUpdated(myProjectJdkConfigurable.selectedProjectJdk, myProject.isDefault) |
而当myLanguageLevelCombo变动时会触发
|
0 1 |
LanguageLevelProjectExtensionImpl.getInstanceImpl(myProject).currentLevel = myLanguageLevelCombo.selectedLevels |
所以最终实际修改的是LanguageLevelProjectExtensionImpl.getInstanceImpl(myProject).currentLevel
也不难发现是SDK选择后生效的代码
|
0 1 |
com.intellij.openapi.roots.ui.configuration.ProjectJdkConfigurable#apply |
而从ProjectJdkTable.getInstance().findJdk()等一系列方法可以快速获取想要的Sdk对象
官方初始化过程可参考
|
0 1 |
com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl#preconfigure |
自动操作GIT
相关操作都在GitRepositoryManager里,UI操作参考GitLogBranchOperationsActionGroup即可
|
0 1 |
GitRepositoryManager.getInstance(this) |
分支UI的操作见plugins/git4idea/resources/META-INF/plugin.xml
|
0 1 2 3 4 5 6 |
<group class="git4idea.ui.branch.GitLogBranchOperationsActionGroup" id="Git.BranchOperationGroup"/> <action class="git4idea.actions.GitCheckoutRevisionAction" id="Git.CheckoutRevision"/> <action class="git4idea.actions.GitCreateNewBranchAction" id="Git.CreateNewBranch"/> git4idea.ui.branch.GitLogBranchOperationsActionGroup#createBranchGroup git4idea.ui.branch.GitLogBranchOperationsActionGroup#createBranchActions |
可以看到右键菜单是固定的一个分组+两个固定操作,分组操作里会根据选中项是否有分支和tag决定是否显示相关操作
git4idea.ui.branch.GitBranchPopupActions.RemoteBranchActions.CheckoutRemoteBranchAction
TODO
git4idea.ui.branch.GitBranchPopupActions.CheckoutAsNewBranch
TODO
com.intellij.vcs.log.ui.actions.RefreshLogAction
|
0 1 2 3 4 5 6 7 8 9 10 |
import com.intellij.vcs.log.impl.VcsProjectLog import com.intellij.vcs.log.impl.VcsLogManager import com.intellij.vcs.log.util.VcsLogUtil import com.intellij.openapi.command.WriteCommandAction var myProjectLog = VcsProjectLog.getInstance(project) var logManager = myProjectLog.logManager var ui = logManager!!.logUis[0] WriteCommandAction.runWriteCommandAction(project) { logManager!!.dataManager!!.refresh(VcsLogUtil.getVisibleRoots(ui)); } |
git4idea.actions.GitFetch
|
0 1 2 3 4 5 6 7 |
//execCmd("git fetch", "$projectDir") import git4idea.fetch.GitFetchSupport import git4idea.GitUtil var fetchSupport = GitFetchSupport.fetchSupport(project) var repos = GitUtil.getRepositories(project) var result = fetchSupport.fetchAllRemotes(repos) result.showNotification() |
com.intellij.openapi.vcs.changes.actions.RefreshAction
其他自动化
可以参考官方MCP实现
或直接翻官方的IDEA源码https://github.com/JetBrains/intellij-community照抄即可
END

