Просмотр исходного кода

docs: update CHANGELOG v1.2, add MCP_SERVER.md, update README + QUICK_REFERENCE

- CHANGELOG.md: comprehensive v1.2 release notes
- MCP_SERVER.md: full MCP server documentation (tools, resources, architecture, security)
- README.md: added Canvas plotting, MCP server features, updated tech stack
- QUICK_REFERENCE.md: updated plot examples, added MCP curl example
- Multiple bug fixes squashed (JS syntax error, MCP event sync, etc.)

Co-Authored-By: Claude <noreply@anthropic.com>
nonvioce 3 месяцев назад
Родитель
Сommit
921604dbab

+ 51 - 109
CHANGELOG.md

@@ -1,125 +1,67 @@
 # 更新日志
 
-所有重要的项目更改都将记录在此文件中。
-
 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
 
-## [未发布]
+## [1.2] - 2026-07-18
 
 ### 新增
-- **LaTeX 渲染功能**
-  - 使用 KaTeX 实时渲染数学公式
-  - `TeXForm[]` 输出自动渲染为美观的数学公式
-  - 支持显示/隐藏 LaTeX 源码
-  - 一键复制 LaTeX 代码
-  - 插件可调用 `Symja.renderLatex()` 渲染公式
-- **Material Design 3 界面**
-  - 采用 Google Material Design 3 设计语言
-  - Material Icons 图标系统
-  - Roboto 字体家族
-  - Material 动画和涟漪效果
-  - 响应式 Material 组件
-  - 优雅的阴影和高度系统
-- 多种绘图类型支持
-  - `Plot[]` - 标准函数图
-  - `ParametricPlot[]` - 参数方程绘图
-  - `PolarPlot[]` - 极坐标绘图
-  - `ListPlot[]` - 列表数据绘图
-- 高级数学功能
-  - 傅里叶变换和逆变换
-  - 拉普拉斯变换和逆变换
-  - 优化函数(Maximize, Minimize)
-  - 表格生成(Table)
-- 变量管理功能
-  - 查看所有已定义变量及其值
-  - 清除特定变量
-  - 变量追踪
-- 格式转换功能
-  - `TeXForm[]` - 转换为 LaTeX 格式(带渲染)
-  - `InputForm[]` - 转换为输入格式
-  - `FullForm[]` - 显示完整形式
-  - `JavaForm[]` - 转换为 Java 代码
-- 增强的绘图功能
-  - 自动坐标轴缩放
-  - 网格线显示
-  - 坐标轴标注
-  - 图像保存为 PNG
-- 更多 Symja 函数支持
-  - 级数求和(Sum)
-  - 求积(Product)
-  - 数值计算(N)
-  - 矩阵运算增强
-- **插件 API 增强**
-  - 添加 `Symja.toTeX()` - LaTeX 转换
-  - 添加 `Symja.renderLatex()` - LaTeX 渲染
+- **MCP 服务器**(Model Context Protocol)
+  - 15 个 MCP 工具:evaluate, differentiate, integrate, solve, simplify, numeric, to_latex, plot, parametric_plot, list_plot, list_plugins, get_plugin, create_plugin, delete_plugin, execute_plugin
+  - 2 个 MCP 资源:variables://current, history://recent
+  - NanoHTTPd 嵌入式 HTTP 服务器,监听 127.0.0.1:8734
+  - Rhino JS 引擎支持 MCP 端插件执行
+  - Android 前台 Service,支持后台持续运行
+  - MCP 事件实时同步到控制台(🌐 MCP 标记)
+  - 设置面板可控开关,SharedPreferences 持久化
+- **Canvas 绘图系统**
+  - PlotRenderer:坐标轴、网格、标签、多曲线颜色区分
+  - 等比例坐标轴(圆不变椭圆)
+  - 手势支持:双指缩放、拖拽平移、双击重置
+  - 高分屏 dpr 适配
+  - 多函数图例(可点击开关)
+  - 💾 保存为 PNG(MediaStore → 系统相册)
+  - 自动检测 Graphics 输出并 Canvas 渲染
+- **14 个默认插件**(从 mathtool2 迁移)
+  - 统计概率:卡方检验、二项分布、超几何分布、线性回归、混合方差、直方图统计、卷积分布列
+  - 数列:错位相减法
+  - 几何:对称变换(点/直线三种模式)、圆的方程
+  - 数值:二分法求根
+  - 向量:三点法向量(Simplify 精确根式)
+- **设置面板**:MCP 开关、端口、字体大小
+- **欢迎页**:版本号 + 快速开始示例
+- **顶部导航栏**:应用名 + MCP 状态灯 + 帮助/插件/设置按钮
 
 ### 改进
-- 升级 Symja 库到 3.1.0 版本
-- 统一所有命令格式,使用 Mathematica 风格语法
-  - 绘图命令:`Plot[]`, `ParametricPlot[]`, `PolarPlot[]`, `ListPlot[]`
-  - 格式转换:`TeXForm[]`, `InputForm[]`, `FullForm[]`
-  - 表格生成:`Table[]`
-- 优化绘图算法,支持参数方程和极坐标
-- 改进变量显示,显示实际值
-- 增强错误处理和用户提示
-- 优化界面响应速度
-- 移除特殊命令格式,统一使用函数调用
-- **全新 Material Design 界面**
-  - 更现代、更美观的视觉设计
-  - 更好的可访问性
-  - 更流畅的动画效果
-  - 更清晰的视觉层次
+- 统一蓝色配色方案(替换粉色主题)
+- 移除 30+ 个 Symja 不支持的无效函数
+- 输入框增设发送按钮
+- 命令历史使用 SharedPreferences 持久化
+- Canvas 高分屏 DPR 适配
+- MCP 轮询仅在服务器开启时运行
 
 ### 修复
-- 修复绘图时的坐标轴显示问题
-- 修复括号匹配检测
-- 修复多行输入模式的问题
-
-## [1.0.0] - 2024-XX-XX
+- MCP `isError` 位置修正为 result 顶层
+- 修复 Symja `*10^` 科学记数法解析
+- 线程安全:evalHistory、initialized、McpService 状态加同步
+- 屏幕旋转锁定(避免 WebView 状态丢失)
+- AlertDialog 泄漏修复
+- 清除 ~60 行重复 CSS
+- 三点法向量 Simplify[] 精确根式简化
 
-### 新增
-- 基础符号计算功能
-  - 微积分(求导、积分、极限)
-  - 代数运算(展开、因式分解、化简)
-  - 方程求解
-  - 矩阵运算
-- 交互式控制台
-  - 命令历史记录
-  - 自动补全
-  - 多行输入
-  - 虚拟键盘
-- 插件系统
-  - JavaScript 插件支持
-  - 插件导入/导出
-  - 内置示例插件
-- 基础绘图功能
-  - 函数图绘制
-  - 图像保存
-- 帮助系统
-  - 完整的函数文档
-  - 使用示例
-  - 插件开发指南
+### 移除
+- FourierTransform、InverseFourierTransform(Symja 不支持)
+- Combinations、NDSolve、RSolve、Interpolation、Residue
+- String* 系列、Quantity/UnitConvert、CForm/FortranForm 等无效函数
+- Switch、Do、While、For(Symja 控制流不支持)
 
-### 技术
-- 基于 Symja 3.0.0
-- Android 原生应用
-- WebView 交互界面
-- Canvas 绘图
+## [1.1] - 2025
 
----
-
-## 版本说明
+### 新增
+- LaTeX 渲染(KaTeX)
+- Material Design 3 界面
+- 插件 API 增强
 
-### 版本号格式
-- **主版本号**:不兼容的 API 修改
-- **次版本号**:向下兼容的功能性新增
-- **修订号**:向下兼容的问题修正
+## [1.0.0] - 2024
 
-### 变更类型
-- **新增**:新功能
-- **改进**:对现有功能的改进
-- **修复**:Bug 修复
-- **移除**:移除的功能
-- **废弃**:即将移除的功能
-- **安全**:安全相关的修复
+- 初始发布,基础符号计算 + 插件系统

+ 127 - 0
MCP_SERVER.md

@@ -0,0 +1,127 @@
+# MCP 服务器文档
+
+IJS 内置 MCP (Model Context Protocol) 服务器,可将 Symja 符号计算能力暴露给 AI 客户端(如 Claude Desktop)。
+
+## 快速开始
+
+1. 打开应用,MCP 服务器默认自动启动(端口 8734)
+2. 在 Claude Desktop 配置中添加:
+
+```json
+{
+  "mcpServers": {
+    "ijs": {
+      "url": "http://127.0.0.1:8734/mcp"
+    }
+  }
+}
+```
+
+3. 如果 Claude Desktop 和 IJS 不在同一台设备,先通过 adb 转发:
+
+```bash
+adb forward tcp:8734 tcp:8734
+```
+
+4. 顶部状态灯变绿表示服务器运行中,灰色表示未启动
+
+## 工具列表
+
+### 核心计算
+
+| 工具 | 参数 | 示例 |
+|---|---|---|
+| `evaluate` | `expression` | `"2+2"` |
+| `differentiate` | `expression, variable` | `"x^3", "x"` |
+| `integrate` | `expression, variable, lower?, upper?` | `"x^2", "x", "0", "1"` |
+| `solve` | `equation, variable` | `"x^2-5*x+6==0", "x"` |
+| `simplify` | `expression` | `"Sin[x]^2+Cos[x]^2"` |
+| `numeric` | `expression, precision?` | `"Pi", "50"` |
+| `to_latex` | `expression` | `"x^2+2*x+1"` |
+
+### 绘图
+
+| 工具 | 参数 | 示例 |
+|---|---|---|
+| `plot` | `function, variable, x_min, x_max` | `"Sin[x]", "x", "0", "2*Pi"` |
+| `parametric_plot` | `x_func, y_func, t_var, t_min, t_max` | `"2*Cos[t]", "2*Sin[t]", "t", "0", "2*Pi"` |
+| `list_plot` | `points` | `"{{1,1},{2,4},{3,9}}"` |
+
+绘图工具返回结构化 JSON,包含 Line/Point 点序列和颜色。控制台自动 Canvas 渲染。
+
+### 插件管理
+
+| 工具 | 参数 | 说明 |
+|---|---|---|
+| `list_plugins` | — | 列出所有已安装插件 |
+| `get_plugin` | `plugin_id` | 获取插件详情及 JS 代码 |
+| `create_plugin` | `id, name, js_code, icon?, command?, description?, inputs?` | 创建/更新插件 |
+| `delete_plugin` | `plugin_id` | 删除插件 |
+| `execute_plugin` | `plugin_id, arguments?` | 执行插件并返回结果 |
+
+## 资源
+
+| URI | 内容 |
+|---|---|
+| `variables://current` | 当前 Symja 会话中已定义的变量 |
+| `history://recent` | 最近通过 MCP 执行的计算历史 |
+
+## 响应格式
+
+所有绘图操作返回结构化 JSON:
+
+```json
+{
+  "type": "plot",
+  "elements": [
+    {
+      "type": "line",
+      "color": [0.368, 0.507, 0.710],
+      "points": [[0.0, 0.0], [0.1, 0.0998], ...]
+    }
+  ],
+  "options": {"axes": true}
+}
+```
+
+工具调用错误时,`result.isError` 为 `true`。
+
+## 插件开发
+
+MCP 可通过 `create_plugin` 工具远程创建插件。插件 JS 代码中可使用完整的 Symja API:
+
+```javascript
+function calculate(inputs, Symja) {
+  Symja.print('计算中...');
+  return Symja.eval('Integrate[' + inputs.func + ', ' + inputs.var + ']');
+}
+```
+
+可用 API:`eval`, `print`, `differentiate`, `integrate`, `solve`, `simplify`, `expand`, `factor`, `plot`, `parametricPlot`, `listPlot`, `numeric`, `toTeX`, `gcd`, `lcm`, `isPrime`, `fibonacci`, `binomial`, `sum`, `product`, `limit`, 及矩阵运算。
+
+## 架构
+
+```
+Claude Desktop ──HTTP──▶ 127.0.0.1:8734/mcp
+                              │
+                    NanoHTTPd (McpServer.java)
+                              │
+                    JSON-RPC 2.0 dispatch
+                              │
+              ┌───────────────┼───────────────┐
+              ▼               ▼               ▼
+        SymjaEngine    PluginManager    Rhino JS
+        (数学计算)      (插件管理)     (插件执行)
+              │
+              ▼
+        AppSession (共享会话 + MCP事件队列)
+              │
+              ▼
+        WebView 控制台 (实时同步显示)
+```
+
+## 安全
+
+- 仅监听 `127.0.0.1`,不对外暴露
+- 无认证(信任本地环境,与主流 MCP 实现一致)
+- 可在设置面板关闭 MCP 服务器

+ 23 - 6
QUICK_REFERENCE.md

@@ -129,14 +129,31 @@ f = x^2 + 3*x
 
 ### 绘制美丽的曲线
 ```mathematica
-// 利萨茹图形
-ParametricPlot[{Sin[3*t], Cos[5*t]}, {t, 0, 2*Pi}]
-
 // 心形线
-PolarPlot[1+Cos[t], {t, 0, 2*Pi}]
+ParametricPlot[{2*Cos[t]-Cos[2*t], 2*Sin[t]-Sin[2*t]}, {t, 0, 2*Pi}]
+
+// 螺旋线
+ParametricPlot[{t*Cos[t], t*Sin[t]}, {t, 0, 6*Pi}]
+
+// 圆
+ParametricPlot[{2*Cos[t], 2*Sin[t]}, {t, 0, 2*Pi}]
 
-// 玫瑰线
-PolarPlot[Sin[6*t], {t, 0, Pi}]
+// 五函数同坐标系
+Plot[{Sin[x], Cos[x], x/3, x^2/8, Sin[2*x]/2}, {x, 0, 2*Pi}]
+
+// 散点图
+ListPlot[{{1,1},{2,4},{3,9},{4,16}}]
+```
+
+### MCP 服务器
+```bash
+# 默认端口 8734,自动启动
+curl -X POST http://127.0.0.1:8734/mcp \
+  -H "Content-Type: application/json" \
+  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
+       "params":{"name":"evaluate",
+                 "arguments":{"expression":"D[x^3,x]"}}}'
+```
 
 // 螺旋线
 ParametricPlot[{t*Cos[t], t*Sin[t]}, {t, 0, 10}]

+ 30 - 5
README.md

@@ -37,12 +37,29 @@
 - 插件导入/导出/分享
 - 命令快捷方式支持
 
+### 🎨 Canvas 绘图
+- `Plot[f,{x,a,b}]` 函数图像绘制
+- `ParametricPlot` 参数方程绘图
+- `ListPlot` 散点图
+- 双指缩放、拖拽平移、双击重置
+- 多函数自动配色 + 图例
+- 等比例坐标轴(圆不变椭圆)
+- 💾 一键保存到系统相册
+
+### 🌐 MCP 服务器
+- 内置 Model Context Protocol 服务器
+- 15 个 MCP 工具 + 2 个资源
+- 支持 Claude Desktop 等 AI 客户端接入
+- MCP 命令实时同步到控制台
+- 端口 8734,设置面板可控开关
+- 详见 [MCP_SERVER.md](MCP_SERVER.md)
+
 ### 💡 智能交互
 - 自动补全函数名和命令
-- ↑↓ 键浏览历史记录
+- ↑↓ 键浏览历史记录(持久化存储)
 - 变量管理和 `%` 引用
-- 虚拟键盘快速输入
 - Jupyter 风格的执行计数器
+- 设置面板:字体大小、MCP 开关
 
 ---
 
@@ -79,6 +96,13 @@ Inverse[{{1,2},{3,4}}]         // 逆矩阵
 Eigenvalues[{{1,2},{3,4}}]     // 特征值
 ```
 
+### 绘图
+```mathematica
+Plot[Sin[x], {x, 0, 2*Pi}]                    // 函数图像
+ParametricPlot[{2*Cos[t], 2*Sin[t]}, {t,0,2*Pi}] // 圆
+ListPlot[{{1,1},{2,4},{3,9}}]                 // 散点
+```
+
 ### LaTeX 渲染
 ```mathematica
 TeXForm[Integrate[x^2, x]]     // 自动渲染为数学公式
@@ -164,11 +188,12 @@ Symja.isPrime(n)
 
 ## 🔧 技术栈
 
-- **Android**:原生 Android 应用(Java 11)
+- **Android**:原生 Android 应用(Java 11, API 24-34)
 - **Symja 3.1.0**:符号计算引擎
-- **WebView + JavaScript**:交互式控制台
+- **NanoHTTPd 2.3.1**:嵌入式 HTTP 服务器(MCP)
+- **Rhino 1.7.15**:JavaScript 引擎(插件执行)
+- **WebView + JavaScript**:交互式控制台 + Canvas 绘图
 - **KaTeX 0.16.9**:LaTeX 渲染
-- **Material Design 3**:现代化 UI 设计
 
 ---
 

+ 2 - 1
app/src/main/AndroidManifest.xml

@@ -25,7 +25,8 @@
         
         <activity
             android:name=".MainActivity"
-            android:exported="true">
+            android:exported="true"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />

+ 95 - 65
app/src/main/assets/console.css

@@ -359,52 +359,6 @@ body {
     background: #2E7D32;
 }
 
-.latex-source {
-    font-family: var(--font-mono);
-    font-size: 12px;
-    color: #616161;
-    background: #F5F5F5;
-    padding: 8px 12px;
-    border-radius: 4px;
-    margin-top: 12px;
-    display: none;
-    word-break: break-all;
-}
-
-.latex-actions {
-    margin-top: 12px;
-    display: flex;
-    gap: 8px;
-}
-
-.latex-btn {
-    padding: 6px 12px;
-    border: none;
-    border-radius: 4px;
-    font-size: 12px;
-    font-weight: 500;
-    cursor: pointer;
-    transition: all 0.2s;
-}
-
-.latex-toggle {
-    background: var(--info-color);
-    color: #FFFFFF;
-}
-
-.latex-toggle:hover {
-    background: #6A1B9A;
-}
-
-.latex-copy {
-    background: var(--success-color);
-    color: #FFFFFF;
-}
-
-.latex-copy:hover {
-    background: #2E7D32;
-}
-
 /* 终端输入行 */
 .terminal-input-line {
     display: flex;
@@ -757,25 +711,6 @@ body {
 }
 
 /* 插件面板 - 保持原样但调整颜色 */
-.plugin-panel {
-    position: fixed;
-    top: 0;
-    right: -100%;
-    width: 90%;
-    max-width: 500px;
-    height: 100vh;
-    background: #FFFFFF;
-    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.2);
-    transition: right 0.3s ease-out;
-    z-index: 1000;
-    display: flex;
-    flex-direction: column;
-}
-
-.plugin-panel.show {
-    right: 0;
-}
-
 /* 响应式设计 */
 @media (max-width: 600px) {
     .top-nav {
@@ -1572,3 +1507,98 @@ body {
     background: var(--primary-color);
     color: #FFFFFF;
 }
+
+/* ── 图例 ───────────────────────────────────────── */
+.plot-legend {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    padding: 6px 8px;
+    font-size: 12px;
+}
+.legend-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    color: #616161;
+}
+.legend-swatch {
+    display: inline-block;
+    width: 14px;
+    height: 3px;
+    border-radius: 2px;
+}
+
+/* ── 设置面板 ────────────────────────────────────── */
+.settings-panel {
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 85%;
+    max-width: 380px;
+    background: #FFFFFF;
+    border-radius: 12px;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
+    z-index: 3000;
+    display: none;
+    overflow: hidden;
+}
+.settings-panel.show {
+    display: block;
+}
+.settings-panel-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 16px 20px;
+    background: var(--primary-color);
+    color: #FFFFFF;
+}
+.settings-panel-header h2 {
+    font-size: 18px;
+    font-weight: 500;
+}
+.settings-content {
+    padding: 20px;
+}
+.setting-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16px;
+}
+.setting-item label {
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--terminal-fg);
+}
+.setting-input {
+    width: 100px;
+    padding: 8px 10px;
+    border: 1px solid var(--terminal-border);
+    border-radius: 6px;
+    font-size: 14px;
+    text-align: center;
+}
+.setting-input:focus {
+    outline: none;
+    border-color: var(--primary-color);
+}
+.setting-select {
+    padding: 8px 10px;
+    border: 1px solid var(--terminal-border);
+    border-radius: 6px;
+    font-size: 14px;
+    min-width: 80px;
+}
+.setting-actions {
+    text-align: center;
+    margin: 16px 0 12px;
+}
+.setting-info {
+    text-align: center;
+    font-size: 12px;
+    color: #9E9E9E;
+    line-height: 1.6;
+}

+ 39 - 6
app/src/main/assets/console.html

@@ -22,12 +22,9 @@
             </div>
             <div class="nav-actions">
                 <span id="mcpStatus" class="mcp-status" title="MCP 服务器状态">⚪</span>
-                <button class="nav-btn" id="helpBtn" title="帮助">
-                    <span class="icon-symbol">?</span>
-                </button>
-                <button class="nav-btn" id="pluginsBtn" title="插件">
-                    <span class="icon-symbol">🔌</span>
-                </button>
+                <button class="nav-btn" id="helpBtn" title="帮助">?</button>
+                <button class="nav-btn" id="pluginsBtn" title="插件">🔌</button>
+                <button class="nav-btn" id="settingsBtn" title="设置">⚙</button>
             </div>
         </header>
 
@@ -329,6 +326,42 @@
         </div>
     </div>
 
+    <!-- 设置面板 -->
+    <div id="settingsPanel" class="settings-panel">
+        <div class="settings-panel-header">
+            <h2>⚙️ 设置</h2>
+            <button id="closeSettings" class="close-btn">✕</button>
+        </div>
+        <div class="settings-content">
+            <div class="setting-item">
+                <label>MCP 服务器</label>
+                <select id="settingMcpEnabled" class="setting-select">
+                    <option value="1">开启</option>
+                    <option value="0">关闭</option>
+                </select>
+            </div>
+            <div class="setting-item">
+                <label>MCP 端口</label>
+                <input type="text" id="settingMcpPort" value="8734" class="setting-input">
+            </div>
+            <div class="setting-item">
+                <label>字体大小</label>
+                <select id="settingFontSize" class="setting-select">
+                    <option value="13">小</option>
+                    <option value="14" selected>中</option>
+                    <option value="16">大</option>
+                </select>
+            </div>
+            <div class="setting-actions">
+                <button id="applySettings" class="action-btn">应用</button>
+            </div>
+            <div class="setting-info">
+                <p>某人的计算器 v1.2</p>
+                <p>Symja 3.1.0 · MCP 2024-11-05</p>
+            </div>
+        </div>
+    </div>
+
     <script src="console.js"></script>
 </body>
 </html>

+ 204 - 53
app/src/main/assets/console.js

@@ -49,14 +49,21 @@ class SymjaConsole {
         // 顶部导航按钮
         this.helpBtn?.addEventListener('click', () => this.showHelp());
         this.pluginsBtn?.addEventListener('click', () => this.showPluginPanel());
+        this.settingsBtn?.addEventListener('click', () => this.showSettings());
 
-        // MCP 状态更新
-        this.updateMcpStatus();
-        setInterval(() => this.updateMcpStatus(), 3000);
+        // 设置面板
+        document.getElementById('closeSettings')?.addEventListener('click', () => this.hideSettings());
+        document.getElementById('applySettings')?.addEventListener('click', () => this.applySettings());
+
+        // 加载已保存的设置
+        this.loadSettings();
 
-        // MCP 事件同步(每秒轮询)
+        // MCP 状态更新和事件轮询(仅在服务器开启时运行)
         this.mcpEventCount = 0;
-        setInterval(() => this.pollMcpEvents(), 1000);
+        this.mcpPollTimer = null;
+        this.mcpStatusTimer = null;
+        this.updateMcpStatus();
+        this.startMcpPollingIfNeeded();
         
         // 快捷键工具栏
         this.initShortcutsBar();
@@ -420,7 +427,20 @@ class SymjaConsole {
     }
 
     showWelcome() {
-        // 不显示欢迎消息
+        const line = document.createElement('div');
+        line.className = 'terminal-line';
+        line.innerHTML = `<div class="welcome-message">
+            <h2>🧮 某人的计算器 v1.2</h2>
+            <p>基于 Symja 的交互式符号计算控制台</p>
+            <p><strong>快速开始:</strong></p>
+            <p><code>2+2</code> 算术运算</p>
+            <p><code>D[x^3, x]</code> 求导</p>
+            <p><code>Integrate[x^2, {x,0,1}]</code> 定积分</p>
+            <p><code>Solve[x^2-5*x+6==0, x]</code> 解方程</p>
+            <p><code>Plot[Sin[x], {x,0,2*Pi}]</code> 函数绘图</p>
+            <p style="margin-top:12px">输入 <code>help</code> 查看完整文档 · 输入 <code>plugins</code> 管理插件</p>
+        </div>`;
+        this.output.appendChild(line);
     }
 
     evaluate() {
@@ -482,7 +502,7 @@ class SymjaConsole {
         this.scrollToBottom();
     }
 
-    addPlotOutput(plotJson, count) {
+    addPlotOutput(plotJson, count, labels) {
         const data = JSON.parse(plotJson);
         const line = document.createElement('div');
         line.className = 'terminal-line';
@@ -492,13 +512,37 @@ class SymjaConsole {
 
         const canvas = document.createElement('canvas');
         canvas.className = 'plot-canvas';
-        canvas.width = 720;
-        canvas.height = 500;
+        const dpr = window.devicePixelRatio || 1;
+        canvas.width = 720 * dpr;
+        canvas.height = 500 * dpr;
+        canvas.style.width = '720px';
+        canvas.style.height = '500px';
         container.appendChild(canvas);
 
-        // 保存按钮
+        // 图例(多曲线时自动显示)
+        let legendVisible = true;
+        let legendEl = null;
+        if (data.elements && data.elements.length > 1) {
+            legendEl = this.buildLegend(data, labels);
+            if (legendEl) container.appendChild(legendEl);
+        }
+
+        // 保存按钮 + 图例开关
         const actions = document.createElement('div');
         actions.className = 'plot-actions';
+
+        if (legendEl) {
+            const legendBtn = document.createElement('button');
+            legendBtn.className = 'plot-save-btn';
+            legendBtn.textContent = '🏷 图例';
+            legendBtn.onclick = () => {
+                legendVisible = !legendVisible;
+                legendEl.style.display = legendVisible ? 'flex' : 'none';
+                legendBtn.style.opacity = legendVisible ? '1' : '0.5';
+            };
+            actions.appendChild(legendBtn);
+        }
+
         const saveBtn = document.createElement('button');
         saveBtn.className = 'plot-save-btn';
         saveBtn.textContent = '💾 保存图片';
@@ -509,13 +553,30 @@ class SymjaConsole {
         line.appendChild(container);
         this.output.appendChild(line);
 
-        // 初始化渲染器
         if (!this.plotRenderer) {
             this.plotRenderer = new PlotRenderer();
         }
         this.plotRenderer.render(canvas, data);
     }
 
+    buildLegend(data, labels) {
+        const div = document.createElement('div');
+        div.className = 'plot-legend';
+        const colors = [
+            [0.369,0.507,0.710],[0.851,0.208,0.125],[0.267,0.612,0.329],
+            [0.737,0.349,0.800],[1.0,0.608,0.0]
+        ];
+        data.elements.forEach((el, i) => {
+            const c = el.color || colors[i % colors.length];
+            const rgb = `rgb(${Math.round(c[0]*255)},${Math.round(c[1]*255)},${Math.round(c[2]*255)})`;
+            const item = document.createElement('span');
+            item.className = 'legend-item';
+            item.innerHTML = `<span class="legend-swatch" style="background:${rgb}"></span>${labels ? labels[i] : '曲线'+(i+1)}`;
+            div.appendChild(item);
+        });
+        return div;
+    }
+
     savePlot(canvas) {
         try {
             const dataUrl = canvas.toDataURL('image/png');
@@ -1112,20 +1173,29 @@ class SymjaConsole {
     }
 
     loadHistory() {
+        try {
+            if (typeof Android !== 'undefined' && Android.loadHistory) {
+                const saved = Android.loadHistory();
+                return saved ? JSON.parse(saved) : [];
+            }
+        } catch (e) {}
+        // 回退到 localStorage
         try {
             const saved = localStorage.getItem('symja_history');
             return saved ? JSON.parse(saved) : [];
-        } catch (e) {
-            return [];
-        }
+        } catch (e) { return []; }
     }
 
     saveHistory() {
+        const json = JSON.stringify(this.history);
         try {
-            localStorage.setItem('symja_history', JSON.stringify(this.history));
-        } catch (e) {
-            // 忽略存储错误
-        }
+            if (typeof Android !== 'undefined' && Android.saveHistory) {
+                Android.saveHistory(json);
+            }
+        } catch (e) {}
+        try {
+            localStorage.setItem('symja_history', json);
+        } catch (e) {}
     }
 
     saveSession(name) {
@@ -1446,9 +1516,92 @@ class SymjaConsole {
     }
     
     showSettings() {
-        // 设置功能待实现
-        this.addOutput('⚙️ 设置功能即将推出...', 'info');
+        const panel = document.getElementById('settingsPanel');
+        if (!panel) return;
+        this.hideKeyboard();
+        document.getElementById('settingMcpEnabled').value = this.getMcpEnabled() ? '1' : '0';
+        document.getElementById('settingMcpPort').value = this.getSetting('mcpPort', '8734');
+        document.getElementById('settingFontSize').value = this.getSetting('fontSize', '14');
+        panel.classList.add('show');
+    }
+
+    hideSettings() {
+        document.getElementById('settingsPanel')?.classList.remove('show');
+    }
+
+    applySettings() {
+        const wasEnabled = this.getMcpEnabled();
+        const enabled = document.getElementById('settingMcpEnabled').value === '1';
+        const port = document.getElementById('settingMcpPort').value;
+        const fontSize = document.getElementById('settingFontSize').value;
+        this.saveSetting('mcpPort', port);
+        this.saveSetting('fontSize', fontSize);
+        this.setMcpEnabled(enabled);
+        document.querySelector('.terminal-output').style.fontSize = fontSize + 'px';
+        document.querySelector('.terminal-input').style.fontSize = fontSize + 'px';
+        this.hideSettings();
+        this.addOutput('⚙️ 设置已应用', 'help');
         this.scrollToBottom();
+
+        // MCP 状态变更时更新轮询
+        if (enabled !== wasEnabled) {
+            if (enabled) {
+                this.startMcpPollingIfNeeded();
+            } else {
+                this.stopMcpPolling();
+            }
+        }
+    }
+
+    getMcpEnabled() {
+        try {
+            if (typeof Android !== 'undefined' && Android.getMcpEnabled !== undefined) {
+                return Android.getMcpEnabled();
+            }
+        } catch(e) {}
+        return this.getSetting('mcpEnabled', '1') === '1';
+    }
+
+    setMcpEnabled(val) {
+        this.saveSetting('mcpEnabled', val ? '1' : '0');
+        try {
+            if (typeof Android !== 'undefined' && Android.setMcpEnabled) {
+                Android.setMcpEnabled(val);
+            }
+        } catch(e) {}
+    }
+
+    startMcpPollingIfNeeded() {
+        if (!this.getMcpEnabled()) return;
+        if (!this.mcpPollTimer) {
+            this.mcpPollTimer = setInterval(() => this.pollMcpEvents(), 1000);
+        }
+        if (!this.mcpStatusTimer) {
+            this.mcpStatusTimer = setInterval(() => this.updateMcpStatus(), 3000);
+        }
+    }
+
+    stopMcpPolling() {
+        if (this.mcpPollTimer) { clearInterval(this.mcpPollTimer); this.mcpPollTimer = null; }
+        if (this.mcpStatusTimer) { clearInterval(this.mcpStatusTimer); this.mcpStatusTimer = null; }
+        const status = document.getElementById('mcpStatus');
+        if (status) { status.className = 'mcp-status'; status.title = 'MCP 已关闭'; }
+    }
+
+    getSetting(key, def) {
+        try { return localStorage.getItem('ijs_' + key) || def; } catch(e) { return def; }
+    }
+
+    saveSetting(key, val) {
+        try { localStorage.setItem('ijs_' + key, val); } catch(e) {}
+    }
+
+    loadSettings() {
+        const fs = this.getSetting('fontSize', '14');
+        try {
+            document.querySelector('.terminal-output').style.fontSize = fs + 'px';
+            document.querySelector('.terminal-input').style.fontSize = fs + 'px';
+        } catch(e) {}
     }
 
     clear() {
@@ -1482,12 +1635,11 @@ class SymjaConsole {
     }
 
     loadExecutionCount() {
-        // 每次打开应用都从 1 开始
-        return 1;
+        return 1;  // 每次打开应用从1开始
     }
 
     saveExecutionCount() {
-        // 不再保存到 localStorage,每次打开应用都重置
+        // 不持久化编号,每次打开都重置
     }
 
     navigateHistory(direction) {
@@ -1886,9 +2038,6 @@ class SymjaConsole {
                     this.addOutput(ev.result, 'mcp-result', { seq: this.mcpEventCount });
                 }
             });
-            this.executionCount++;
-            this.saveExecutionCount();
-            this.updatePrompt();
             this.scrollToBottom();
         } catch (e) {
             // Ignore polling errors
@@ -2151,53 +2300,55 @@ class PlotRenderer {
     // ── 手势 ──────────────────────────────────────────
     bindGestures() {
         const canvas = this.canvas;
-        let lastDist = 0, lastX = 0, lastY = 0;
-        let lastTap = 0;
+        // 移除旧监听器防止泄露
+        if (this._touchHandlers) {
+            canvas.removeEventListener('touchstart', this._touchHandlers.start);
+            canvas.removeEventListener('touchmove', this._touchHandlers.move);
+            canvas.removeEventListener('touchend', this._touchHandlers.end);
+        }
+        let lastDist = 0, lastX = 0, lastY = 0, lastTap = 0;
+        const self = this;
 
-        canvas.addEventListener('touchstart', (e) => {
+        const onStart = (e) => {
             if (e.touches.length === 2) {
-                lastDist = Math.hypot(
-                    e.touches[0].clientX - e.touches[1].clientX,
-                    e.touches[0].clientY - e.touches[1].clientY
-                );
+                lastDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
             } else if (e.touches.length === 1) {
                 lastX = e.touches[0].clientX;
                 lastY = e.touches[0].clientY;
             }
             e.preventDefault();
-        }, { passive: false });
-
-        canvas.addEventListener('touchmove', (e) => {
+        };
+        const onMove = (e) => {
             if (e.touches.length === 2) {
-                const dist = Math.hypot(
-                    e.touches[0].clientX - e.touches[1].clientX,
-                    e.touches[0].clientY - e.touches[1].clientY
-                );
+                const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
                 const scale = dist / lastDist;
-                this.transform.sx = Math.max(0.1, Math.min(20, this.transform.sx / scale));
-                this.transform.sy = Math.max(0.1, Math.min(20, this.transform.sy / scale));
+                self.transform.sx = Math.max(0.1, Math.min(20, self.transform.sx / scale));
+                self.transform.sy = Math.max(0.1, Math.min(20, self.transform.sy / scale));
                 lastDist = dist;
-                this.draw();
+                self.draw();
             } else if (e.touches.length === 1) {
                 const dx = (e.touches[0].clientX - lastX) / canvas.clientWidth;
                 const dy = (e.touches[0].clientY - lastY) / canvas.clientHeight;
-                this.transform.ox -= dx / this.transform.sx;
-                this.transform.oy += dy / this.transform.sy;
+                self.transform.ox -= dx / self.transform.sx;
+                self.transform.oy += dy / self.transform.sy;
                 lastX = e.touches[0].clientX;
                 lastY = e.touches[0].clientY;
-                this.draw();
+                self.draw();
             }
             e.preventDefault();
-        }, { passive: false });
-
-        canvas.addEventListener('touchend', (e) => {
+        };
+        const onEnd = (e) => {
             const now = Date.now();
             if (now - lastTap < 300) {
-                // 双击重置
-                this.transform = { ox: 0, oy: 0, sx: 1, sy: 1 };
-                this.draw();
+                self.transform = { ox: 0, oy: 0, sx: 1, sy: 1 };
+                self.draw();
             }
             lastTap = now;
-        });
+        };
+
+        canvas.addEventListener('touchstart', onStart, { passive: false });
+        canvas.addEventListener('touchmove', onMove, { passive: false });
+        canvas.addEventListener('touchend', onEnd);
+        this._touchHandlers = { start: onStart, move: onMove, end: onEnd };
     }
 }

+ 64 - 13
app/src/main/java/com/nonvioce/ijs/MainActivity.java

@@ -17,6 +17,7 @@ public class MainActivity extends AppCompatActivity {
     private ActivityMainBinding binding;
     private SymjaEngine symjaEngine;
     private PluginManager pluginManager;
+    private android.app.AlertDialog currentDialog;
 
     @SuppressLint("SetJavaScriptEnabled")
     @Override
@@ -40,27 +41,27 @@ public class MainActivity extends AppCompatActivity {
             @Override
             public boolean onJsConfirm(android.webkit.WebView view, String url, String message,
                                       android.webkit.JsResult result) {
-                new android.app.AlertDialog.Builder(MainActivity.this)
+                currentDialog = new android.app.AlertDialog.Builder(MainActivity.this)
                     .setTitle("确认")
                     .setMessage(message)
-                    .setPositiveButton("确定", (dialog, which) -> result.confirm())
-                    .setNegativeButton("取消", (dialog, which) -> result.cancel())
-                    .setOnCancelListener(dialog -> result.cancel())
-                    .create()
-                    .show();
+                    .setPositiveButton("确定", (dialog, which) -> { result.confirm(); currentDialog = null; })
+                    .setNegativeButton("取消", (dialog, which) -> { result.cancel(); currentDialog = null; })
+                    .setOnCancelListener(dialog -> { result.cancel(); currentDialog = null; })
+                    .create();
+                currentDialog.show();
                 return true;
             }
 
             @Override
             public boolean onJsAlert(android.webkit.WebView view, String url, String message,
                                     android.webkit.JsResult result) {
-                new android.app.AlertDialog.Builder(MainActivity.this)
+                currentDialog = new android.app.AlertDialog.Builder(MainActivity.this)
                     .setTitle("提示")
                     .setMessage(message)
-                    .setPositiveButton("确定", (dialog, which) -> result.confirm())
-                    .setOnCancelListener(dialog -> result.confirm())
-                    .create()
-                    .show();
+                    .setPositiveButton("确定", (dialog, which) -> { result.confirm(); currentDialog = null; })
+                    .setOnCancelListener(dialog -> { result.confirm(); currentDialog = null; })
+                    .create();
+                currentDialog.show();
                 return true;
             }
         });
@@ -71,12 +72,22 @@ public class MainActivity extends AppCompatActivity {
         // MCP server toggle
         setupMcpToggle();
 
-        // Auto-start MCP server on first launch (for testing convenience)
-        if (!McpService.isRunning()) {
+        // Auto-start MCP server if enabled in settings (default: on)
+        android.content.SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+        if (prefs.getBoolean("mcp_enabled", true) && !McpService.isRunning()) {
             startMcpServer(DEFAULT_MCP_PORT);
         }
     }
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (currentDialog != null && currentDialog.isShowing()) {
+            currentDialog.dismiss();
+            currentDialog = null;
+        }
+    }
+
     // ── MCP toggle ────────────────────────────────────────────────
 
     private void setupMcpToggle() {
@@ -440,6 +451,46 @@ public class MainActivity extends AppCompatActivity {
             }
         }
 
+        @JavascriptInterface
+        public String loadHistory() {
+            android.content.SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+            return prefs.getString("console_history", "[]");
+        }
+
+        @JavascriptInterface
+        public void saveHistory(String historyJson) {
+            android.content.SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+            prefs.edit().putString("console_history", historyJson).apply();
+        }
+
+        @JavascriptInterface
+        public int loadExecutionCount() {
+            return getPreferences(MODE_PRIVATE).getInt("execution_count", 1);
+        }
+
+        @JavascriptInterface
+        public void saveExecutionCount(int count) {
+            getPreferences(MODE_PRIVATE).edit().putInt("execution_count", count).apply();
+        }
+
+        @JavascriptInterface
+        public void setMcpEnabled(boolean enabled) {
+            getPreferences(MODE_PRIVATE).edit().putBoolean("mcp_enabled", enabled).apply();
+            runOnUiThread(() -> {
+                if (enabled && !McpService.isRunning()) {
+                    startMcpServer(DEFAULT_MCP_PORT);
+                } else if (!enabled && McpService.isRunning()) {
+                    stopMcpServer();
+                }
+                updateMcpFabIcon(binding.mcpToggleFab);
+            });
+        }
+
+        @JavascriptInterface
+        public boolean getMcpEnabled() {
+            return getPreferences(MODE_PRIVATE).getBoolean("mcp_enabled", true);
+        }
+
         @JavascriptInterface
         public String getMcpEvents() {
             return AppSession.drainMcpEvents();

+ 10 - 8
app/src/main/java/com/nonvioce/ijs/McpServer.java

@@ -31,8 +31,8 @@ public class McpServer extends NanoHTTPD {
 
     private final SymjaEngine symjaEngine;
     private final PluginManager pluginManager;
-    private final List<String> evalHistory = new ArrayList<>();
-    private boolean initialized = false;
+    private final List<String> evalHistory = java.util.Collections.synchronizedList(new ArrayList<>());
+    private volatile boolean initialized = false;
 
     public McpServer(int port, SymjaEngine symjaEngine, PluginManager pluginManager) {
         super("127.0.0.1", port);
@@ -396,23 +396,25 @@ public class McpServer extends NanoHTTPD {
         JSONObject item = new JSONObject();
         item.put("type", "text");
         item.put("text", textResult);
-        if (textResult.startsWith("Error:")) {
-            item.put("isError", true);
-        }
         content.put(item);
 
         JSONObject result = new JSONObject();
         result.put("content", content);
+        // MCP spec: isError at result top level
+        if (textResult.startsWith("Error:") || textResult.startsWith("错误:")) {
+            result.put("isError", true);
+        }
         return result;
     }
 
     // ── Tool implementations ──────────────────────────────────────
 
     private String callEvaluate(String expression) {
-        // 使用 evaluatePlot 以自动解析 Graphics 为结构化 JSON
         String result = symjaEngine.evaluatePlot(expression);
-        if (evalHistory.size() >= 100) evalHistory.remove(0);
-        evalHistory.add(expression + " → " + result);
+        synchronized (evalHistory) {
+            if (evalHistory.size() >= 100) evalHistory.remove(0);
+            evalHistory.add(expression + " → " + result);
+        }
         AppSession.publishMcpEvent(expression, result);
         return result;
     }

+ 2 - 2
app/src/main/java/com/nonvioce/ijs/McpService.java

@@ -28,8 +28,8 @@ public class McpService extends Service {
     public static final String ACTION_STOP  = "com.nonvioce.ijs.action.MCP_STOP";
     public static final String EXTRA_PORT   = "com.nonvioce.ijs.extra.MCP_PORT";
 
-    private static boolean running = false;
-    private static int currentPort = DEFAULT_PORT;
+    private static volatile boolean running = false;
+    private static volatile int currentPort = DEFAULT_PORT;
 
     private McpServer server;
     private Thread serverThread;