cmdbox 0.6.1__py3-none-any.whl → 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cmdbox might be problematic. Click here for more details.
- cmdbox/app/auth/azure_signin.py +5 -1
- cmdbox/app/auth/signin.py +7 -3
- cmdbox/app/edge.py +3 -3
- cmdbox/app/features/cli/agent_base.py +42 -42
- cmdbox/app/features/cli/audit_base.py +28 -28
- cmdbox/app/features/cli/cmdbox_audit_createdb.py +28 -28
- cmdbox/app/features/cli/cmdbox_audit_delete.py +26 -26
- cmdbox/app/features/cli/cmdbox_audit_search.py +42 -42
- cmdbox/app/features/cli/cmdbox_audit_write.py +22 -22
- cmdbox/app/features/cli/cmdbox_client_file_copy.py +36 -36
- cmdbox/app/features/cli/cmdbox_client_file_download.py +38 -38
- cmdbox/app/features/cli/cmdbox_client_file_list.py +34 -34
- cmdbox/app/features/cli/cmdbox_client_file_mkdir.py +32 -32
- cmdbox/app/features/cli/cmdbox_client_file_move.py +34 -34
- cmdbox/app/features/cli/cmdbox_client_file_remove.py +32 -32
- cmdbox/app/features/cli/cmdbox_client_file_rmdir.py +32 -32
- cmdbox/app/features/cli/cmdbox_client_file_upload.py +38 -38
- cmdbox/app/features/cli/cmdbox_client_server_info.py +26 -26
- cmdbox/app/features/cli/cmdbox_cmd_list.py +22 -21
- cmdbox/app/features/cli/cmdbox_cmd_load.py +24 -20
- cmdbox/app/features/cli/cmdbox_edge_config.py +40 -40
- cmdbox/app/features/cli/cmdbox_edge_start.py +4 -4
- cmdbox/app/features/cli/cmdbox_gui_start.py +2 -2
- cmdbox/app/features/cli/cmdbox_gui_stop.py +2 -2
- cmdbox/app/features/cli/cmdbox_mcp_proxy.py +17 -11
- cmdbox/app/features/cli/cmdbox_server_list.py +20 -20
- cmdbox/app/features/cli/cmdbox_server_start.py +26 -26
- cmdbox/app/features/cli/cmdbox_server_stop.py +26 -26
- cmdbox/app/features/cli/cmdbox_web_apikey_add.py +24 -24
- cmdbox/app/features/cli/cmdbox_web_apikey_del.py +24 -24
- cmdbox/app/features/cli/cmdbox_web_gencert.py +24 -24
- cmdbox/app/features/cli/cmdbox_web_genpass.py +20 -20
- cmdbox/app/features/cli/cmdbox_web_group_add.py +26 -26
- cmdbox/app/features/cli/cmdbox_web_group_del.py +22 -22
- cmdbox/app/features/cli/cmdbox_web_group_edit.py +26 -26
- cmdbox/app/features/cli/cmdbox_web_group_list.py +22 -22
- cmdbox/app/features/cli/cmdbox_web_start.py +66 -66
- cmdbox/app/features/cli/cmdbox_web_stop.py +10 -10
- cmdbox/app/features/cli/cmdbox_web_user_add.py +32 -32
- cmdbox/app/features/cli/cmdbox_web_user_del.py +22 -22
- cmdbox/app/features/cli/cmdbox_web_user_edit.py +32 -32
- cmdbox/app/features/cli/cmdbox_web_user_list.py +22 -22
- cmdbox/app/features/web/cmdbox_web_agent.py +0 -4
- cmdbox/app/mcp.py +316 -120
- cmdbox/app/options.py +21 -21
- cmdbox/app/web.py +1 -1
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_client_time.py +4 -4
- cmdbox/extensions/sample_project/sample/app/features/cli/sample_server_time.py +18 -18
- cmdbox/extensions/sample_project/sample/extensions/user_list.yml +4 -0
- cmdbox/extensions/user_list.yml +4 -0
- cmdbox/licenses/LICENSE_dnspython_2_7_0_ISC_License-ISCL.txt +35 -0
- cmdbox/licenses/LICENSE_email_validator_2_2_0_The_Unlicense-Unlicense.txt +27 -0
- cmdbox/licenses/files.txt +4 -2
- cmdbox/version.py +2 -2
- cmdbox/web/agent.html +2 -2
- cmdbox/web/assets/cmdbox/audit.js +14 -14
- cmdbox/web/assets/cmdbox/common.js +21 -7
- cmdbox/web/assets/cmdbox/list_cmd.js +5 -5
- cmdbox/web/assets/cmdbox/signin.js +17 -7
- cmdbox/web/assets/cmdbox/svgicon.js +3 -3
- cmdbox/web/assets/cmdbox/users.js +14 -5
- cmdbox/web/audit.html +6 -6
- cmdbox/web/signin.html +33 -7
- cmdbox/web/users.html +4 -4
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/METADATA +27 -23
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/RECORD +72 -70
- /cmdbox/licenses/{LICENSE_fastmcp_2_9_2_Apache_Software_License.txt → LICENSE_fastmcp_2_10_1_Apache_Software_License.txt} +0 -0
- /cmdbox/licenses/{LICENSE_mcp_1_9_4_MIT_License.txt → LICENSE_mcp_1_10_1_MIT_License.txt} +0 -0
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/LICENSE +0 -0
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/WHEEL +0 -0
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/entry_points.txt +0 -0
- {cmdbox-0.6.1.dist-info → cmdbox-0.6.2.dist-info}/top_level.txt +0 -0
cmdbox/app/mcp.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from cmdbox.app import common,
|
|
1
|
+
from cmdbox.app import common, feature
|
|
2
2
|
from cmdbox.app.options import Options
|
|
3
3
|
from cmdbox.app.auth import signin
|
|
4
4
|
from pathlib import Path
|
|
@@ -35,12 +35,14 @@ class Mcp:
|
|
|
35
35
|
self.ver = ver
|
|
36
36
|
self.signin = sign
|
|
37
37
|
|
|
38
|
-
def create_mcpserver(self, args:argparse.Namespace) -> Any:
|
|
38
|
+
def create_mcpserver(self, args:argparse.Namespace, tools:List[Any], web:Any) -> Any:
|
|
39
39
|
"""
|
|
40
40
|
mcpserverを作成します
|
|
41
41
|
|
|
42
42
|
Args:
|
|
43
43
|
args (argparse.Namespace): 引数
|
|
44
|
+
tools (List[Any]): ツールのリスト
|
|
45
|
+
web (Any): Web関連のオブジェクト
|
|
44
46
|
|
|
45
47
|
Returns:
|
|
46
48
|
Any: FastMCP
|
|
@@ -58,10 +60,13 @@ class Mcp:
|
|
|
58
60
|
issuer=issuer,
|
|
59
61
|
audience=audience
|
|
60
62
|
)
|
|
61
|
-
mcp = FastMCP(name=self.ver.__appid__, auth=auth)
|
|
63
|
+
mcp = FastMCP(name=self.ver.__appid__, tools=tools, auth=auth)
|
|
62
64
|
else:
|
|
63
65
|
self.logger.info(f"Using BearerAuthProvider without public key, issuer, or audience.")
|
|
64
|
-
mcp = FastMCP(name=self.ver.__appid__)
|
|
66
|
+
mcp = FastMCP(name=self.ver.__appid__, tools=tools)
|
|
67
|
+
mcp.add_middleware(self.create_mw_logging(self.logger, args))
|
|
68
|
+
mcp.add_middleware(self.create_mw_reqscope(self.logger, args, web))
|
|
69
|
+
mcp.add_middleware(self.create_mw_toollist(self.logger, args))
|
|
65
70
|
return mcp
|
|
66
71
|
|
|
67
72
|
def create_session_service(self, args:argparse.Namespace) -> Any:
|
|
@@ -119,7 +124,8 @@ class Mcp:
|
|
|
119
124
|
f"ユーザーがコマンドを実行したいとき、あなたは以下の手順に従ってコマンドを確実に実行してください。\n" + \
|
|
120
125
|
f"1. ユーザーのクエリからが実行したいコマンドを特定します。\n" + \
|
|
121
126
|
f"2. コマンド実行に必要なパラメータのなかで、ユーザーのクエリから取得できないものは、コマンド定義にあるデフォルト値を指定して実行してください。\n" + \
|
|
122
|
-
f"3. もしエラーが発生した場合は、ユーザーにコマンド名とパラメータとエラー内容を提示してください。\n"
|
|
127
|
+
f"3. もしエラーが発生した場合は、ユーザーにコマンド名とパラメータとエラー内容を提示してください。\n" \
|
|
128
|
+
f"4. コマンドの実行結果は、json文字列で出力するようにしてください。この時json文字列は「```json」と「```」で囲んだ文字列にしてください。\n"
|
|
123
129
|
|
|
124
130
|
description = description if is_japan else \
|
|
125
131
|
f"Command offer registered in {self.ver.__appid__}."
|
|
@@ -128,7 +134,8 @@ class Mcp:
|
|
|
128
134
|
f"When a user wants to execute a command, you follow these steps to ensure that the command is executed.\n" + \
|
|
129
135
|
f"1. Identify the command you want to execute from the user's query.\n" + \
|
|
130
136
|
f"2. Any parameters required to execute the command that cannot be obtained from the user's query should be executed with the default values provided in the command definition.\n" + \
|
|
131
|
-
f"3. If an error occurs, provide the user with the command name, parameters, and error description.\n"
|
|
137
|
+
f"3. If an error occurs, provide the user with the command name, parameters, and error description.\n" \
|
|
138
|
+
f"4. The result of the command execution should be output as a json string. The json string should be a string enclosed in '```json' and '```'."
|
|
132
139
|
|
|
133
140
|
description = args.agent_description if args.agent_description else description
|
|
134
141
|
instruction = args.agent_instruction if args.agent_instruction else instruction
|
|
@@ -235,13 +242,311 @@ class Mcp:
|
|
|
235
242
|
session_service=session_service,
|
|
236
243
|
)
|
|
237
244
|
|
|
238
|
-
def
|
|
245
|
+
def _to_schema(self, o:Dict[str, Any], is_japan:bool) -> Dict[str, Any]:
|
|
246
|
+
t, m = o["type"], o["multi"]
|
|
247
|
+
title = o['opt'].title().replace('_', ' ')
|
|
248
|
+
description = o['description_ja'] if is_japan else o['description_en']
|
|
249
|
+
if t == Options.T_BOOL:
|
|
250
|
+
return dict(title=title, type="array", items=dict(type="boolean"), description=description) if m \
|
|
251
|
+
else dict(title=title, type="boolean", description=description)
|
|
252
|
+
if t == Options.T_DATE:
|
|
253
|
+
return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
|
|
254
|
+
else dict(title=title, type="string", description=description)
|
|
255
|
+
if t == Options.T_DATETIME:
|
|
256
|
+
return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
|
|
257
|
+
else dict(title=title, type="string", description=description)
|
|
258
|
+
if t == Options.T_DICT:
|
|
259
|
+
return dict(title=title, type="array", items=dict(additionalProperties=True, type="object"), description=description) if m \
|
|
260
|
+
else dict(title=title, type="object", description=description)
|
|
261
|
+
if t == Options.T_DIR or t == Options.T_FILE:
|
|
262
|
+
return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
|
|
263
|
+
else dict(title=title, type="string", description=description)
|
|
264
|
+
if t == Options.T_FLOAT:
|
|
265
|
+
return dict(title=title, type="array", items=dict(type="number"), description=description) if m \
|
|
266
|
+
else dict(title=title, type="number", description=description)
|
|
267
|
+
if t == Options.T_INT:
|
|
268
|
+
return dict(title=title, type="array", items=dict(type="integer"), description=description) if m \
|
|
269
|
+
else dict(title=title, type="integer", description=description)
|
|
270
|
+
if t == Options.T_STR or t == Options.T_TEXT:
|
|
271
|
+
return dict(title=title, type="array", items=dict(type="string"), description=description) if m \
|
|
272
|
+
else dict(title=title, type="string", description=description)
|
|
273
|
+
raise ValueError(f"Unknown type: {t} for option {o['opt']}")
|
|
274
|
+
|
|
275
|
+
def _ds(self, d:str) -> str:
|
|
276
|
+
return f'"{d}"' if d is not None else 'None'
|
|
277
|
+
|
|
278
|
+
def _doc_arg_type(self, o:Dict[str, Any], use_default:True) -> str:
|
|
279
|
+
t, m, d, r = o["type"], o["multi"], o["default"], o["required"]
|
|
280
|
+
ret = ""
|
|
281
|
+
dft = "None"
|
|
282
|
+
if t == Options.T_BOOL:
|
|
283
|
+
ret = "List[bool]" if m else f"bool"
|
|
284
|
+
dft = "[]" if m else f"{d}"
|
|
285
|
+
elif t == Options.T_DATE:
|
|
286
|
+
ret = "List[str]" if m else f"str"
|
|
287
|
+
dft = "[]" if m else self._ds(d)
|
|
288
|
+
elif t == Options.T_DATETIME:
|
|
289
|
+
ret = "List[str]" if m else f"str"
|
|
290
|
+
dft = "[]" if m else self._ds(d)
|
|
291
|
+
elif t == Options.T_DICT:
|
|
292
|
+
ret = "Dict" if m else f"Dict"
|
|
293
|
+
dft = "{}" if m else self._ds(d)
|
|
294
|
+
elif t == Options.T_DIR or t == Options.T_FILE:
|
|
295
|
+
if d is not None: d = str(d).replace('\\', '/')
|
|
296
|
+
ret = "List[str]" if m else f"str"
|
|
297
|
+
dft = "[]" if m else self._ds(d)
|
|
298
|
+
elif t == Options.T_FLOAT:
|
|
299
|
+
ret ="List[float]" if m else f"float"
|
|
300
|
+
dft ="[]" if m else f"{d}"
|
|
301
|
+
elif t == Options.T_INT:
|
|
302
|
+
ret = "List[int]" if m else f"int"
|
|
303
|
+
dft = "[]" if m else f"{d}"
|
|
304
|
+
elif t == Options.T_STR or t == Options.T_TEXT:
|
|
305
|
+
ret = "List[str]" if m else f"str"
|
|
306
|
+
dft = "[]" if m else self._ds(d)
|
|
307
|
+
else:
|
|
308
|
+
raise ValueError(f"Unknown type: {t} for option {o['opt']}")
|
|
309
|
+
return f"{ret}={dft}" if use_default else ret
|
|
310
|
+
|
|
311
|
+
def _doc_arg(self, o:Dict[str, Any], is_japan) -> str:
|
|
312
|
+
s = f' {o["opt"]}:{self._doc_arg_type(o, True)} '
|
|
313
|
+
s += f'{o["description_ja"] if is_japan else o["description_en"]}'
|
|
314
|
+
return s
|
|
315
|
+
|
|
316
|
+
def _create_func_txt(self, func_name:str, mode:str, cmd:str, is_japan:bool, options:Options, title:str='') -> str:
|
|
317
|
+
description = options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
|
|
318
|
+
choices = options.get_cmd_choices(mode, cmd, False)
|
|
319
|
+
func_doc_args = "\n".join([self._doc_arg(o, is_japan) for o in choices])
|
|
320
|
+
func_txt = f"def {func_name}(*args, **kwargs):\n"
|
|
321
|
+
func_txt += f' """\n'
|
|
322
|
+
func_txt += f' {func_name} - MCP Tool Function\n'
|
|
323
|
+
func_txt += f' {description}\n'
|
|
324
|
+
func_txt += f'\n'
|
|
325
|
+
func_txt += f' Args:\n'
|
|
326
|
+
func_txt += f' {func_doc_args}\n'
|
|
327
|
+
func_txt += f'\n'
|
|
328
|
+
func_txt += f' Returns:\n'
|
|
329
|
+
func_txt += f' Dict[str, Any]: 実行結果\n'
|
|
330
|
+
func_txt += f' """\n'
|
|
331
|
+
func_txt += f' logger = logging.getLogger("web")\n'
|
|
332
|
+
func_txt += ' opt = {o["opt"]: kwargs.get(o["opt"], o["default"]) for o in options.get_cmd_choices("'+mode+'", "'+cmd+'", False)}\n'
|
|
333
|
+
func_txt += f' opt["data"] = Path(opt["data"]) if hasattr(opt, "data") else common.HOME_DIR / f".{self.ver.__appid__}"\n'
|
|
334
|
+
func_txt += f' if "{title}":\n'
|
|
335
|
+
func_txt += f' opt_path = opt["data"] / ".cmds" / f"cmd-{title}.json"\n'
|
|
336
|
+
func_txt += f' opt.update(common.loadopt(opt_path))\n'
|
|
337
|
+
func_txt += f' scope = signin.get_request_scope()\n'
|
|
338
|
+
func_txt += f' opt["mode"] = "{mode}"\n'
|
|
339
|
+
func_txt += f' opt["cmd"] = "{cmd}"\n'
|
|
340
|
+
func_txt += f' opt["format"] = False\n'
|
|
341
|
+
func_txt += f' opt["output_json"] = None\n'
|
|
342
|
+
func_txt += f' opt["output_json_append"] = False\n'
|
|
343
|
+
func_txt += f' opt["debug"] = logger.level == logging.DEBUG\n'
|
|
344
|
+
func_txt += f' opt["signin_file"] = scope["web"].signin_file\n'
|
|
345
|
+
func_txt += f' args = argparse.Namespace(**opt)\n'
|
|
346
|
+
func_txt += f' signin_data = signin.Signin.load_signin_file(args.signin_file)\n'
|
|
347
|
+
func_txt += f' req = scope["req"] if scope["req"] is not None else scope["websocket"]\n'
|
|
348
|
+
func_txt += f' sign = signin.Signin._check_signin(req, scope["res"], signin_data, logger)\n'
|
|
349
|
+
func_txt += f' if sign is not None:\n'
|
|
350
|
+
func_txt += f' logger.warning("Unable to execute command because authentication information cannot be obtained")\n'
|
|
351
|
+
func_txt += f' return dict(warn="Unable to execute command because authentication information cannot be obtained")\n'
|
|
352
|
+
func_txt += f' groups = req.session["signin"]["groups"]\n'
|
|
353
|
+
func_txt += f' if not signin.Signin._check_cmd(signin_data, groups, "{mode}", "{cmd}", logger):\n'
|
|
354
|
+
func_txt += f' logger.warning("You do not have permission to execute this command.")\n'
|
|
355
|
+
func_txt += f' return dict(warn="You do not have permission to execute this command.")\n'
|
|
356
|
+
func_txt += f' feat = options.get_cmd_attr("{mode}", "{cmd}", "feature")\n'
|
|
357
|
+
func_txt += f' args.groups = groups\n'
|
|
358
|
+
func_txt += f' try:\n'
|
|
359
|
+
func_txt += f' st, ret, _ = feat.apprun(logger, args, time.perf_counter(), [])\n'
|
|
360
|
+
func_txt += f' return ret\n'
|
|
361
|
+
func_txt += f' except Exception as e:\n'
|
|
362
|
+
func_txt += f' logger.error("Error occurs when tool is executed:", exc_info=True)\n'
|
|
363
|
+
func_txt += f' raise e\n'
|
|
364
|
+
func_txt += f'func_ctx.append({func_name})\n'
|
|
365
|
+
return func_txt
|
|
366
|
+
|
|
367
|
+
def create_tools(self, logger:logging.Logger, args:argparse.Namespace) -> List[Any]:
|
|
368
|
+
"""
|
|
369
|
+
ツールリストを作成します
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
logger (logging.Logger): ロガー
|
|
373
|
+
args (argparse.Namespace): 引数
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List[Any]: fastmcp.tools.FunctionToolのリスト
|
|
377
|
+
"""
|
|
378
|
+
from fastmcp.tools import FunctionTool
|
|
379
|
+
options = Options.getInstance()
|
|
380
|
+
language, _ = locale.getlocale()
|
|
381
|
+
is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
|
|
382
|
+
func_tools:List[FunctionTool] = []
|
|
383
|
+
for mode in options.get_mode_keys():
|
|
384
|
+
for cmd in options.get_cmd_keys(mode):
|
|
385
|
+
if not options.get_cmd_attr(mode, cmd, 'use_agent'):
|
|
386
|
+
continue
|
|
387
|
+
# コマンドの説明と選択肢を取得
|
|
388
|
+
description = options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
|
|
389
|
+
choices = options.get_cmd_choices(mode, cmd, False)
|
|
390
|
+
func_name = f"{mode}_{cmd}"
|
|
391
|
+
# 関数の定義を生成
|
|
392
|
+
func_txt = self._create_func_txt(func_name, mode, cmd, is_japan, options)
|
|
393
|
+
if logger.level == logging.DEBUG:
|
|
394
|
+
logger.debug(f"generating agent tool: {func_name}")
|
|
395
|
+
func_ctx = []
|
|
396
|
+
# 関数を実行してコンテキストに追加
|
|
397
|
+
exec(func_txt,
|
|
398
|
+
dict(time=time,List=List, Path=Path, argparse=argparse, common=common, options=options, logging=logging, signin=signin,),
|
|
399
|
+
dict(func_ctx=func_ctx))
|
|
400
|
+
# 関数のスキーマを生成
|
|
401
|
+
input_schema = dict(
|
|
402
|
+
type="object",
|
|
403
|
+
properties={o['opt']: self._to_schema(o, is_japan) for o in choices},
|
|
404
|
+
required=[o['opt'] for o in choices if o['required']],
|
|
405
|
+
)
|
|
406
|
+
output_schema = dict(type="object", properties=dict())
|
|
407
|
+
func_tool = FunctionTool(fn=func_ctx[0], name=func_name, title=func_name.title(), description=description,
|
|
408
|
+
tags=[f"mode={mode}", f"cmd={cmd}"],
|
|
409
|
+
parameters=input_schema, output_schema=output_schema,)
|
|
410
|
+
# ツールリストに追加
|
|
411
|
+
func_tools.append(func_tool)
|
|
412
|
+
return func_tools
|
|
413
|
+
|
|
414
|
+
def create_mw_toollist(self, logger:logging.Logger, args:argparse.Namespace) -> Any:
|
|
415
|
+
"""
|
|
416
|
+
ツールリストを作成するミドルウェアを作成します
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
logger (logging.Logger): ロガー
|
|
420
|
+
args (argparse.Namespace): 引数
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Any: ミドルウェア
|
|
424
|
+
"""
|
|
425
|
+
from cmdbox.app.web import Web
|
|
426
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext, ListToolsResult
|
|
427
|
+
from fastmcp.tools import FunctionTool
|
|
428
|
+
func_tools:List[FunctionTool] = self.create_tools(logger, args)
|
|
429
|
+
options = Options.getInstance()
|
|
430
|
+
cmd_list:feature.Feature = options.get_cmd_attr('cmd', 'list', "feature")
|
|
431
|
+
language, _ = locale.getlocale()
|
|
432
|
+
is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
|
|
433
|
+
mcp = self
|
|
434
|
+
class CommandListMiddleware(Middleware):
|
|
435
|
+
async def on_list_tools(self, context: MiddlewareContext, call_next):
|
|
436
|
+
# 認証情報の取得
|
|
437
|
+
scope = signin.get_request_scope()
|
|
438
|
+
web:Web = scope["web"]
|
|
439
|
+
signin_file = web.signin_file
|
|
440
|
+
signin_data = signin.Signin.load_signin_file(signin_file)
|
|
441
|
+
if signin.Signin._check_signin(scope["req"], scope["res"], signin_data, logger) is not None:
|
|
442
|
+
logger.warning("Unable to execute command because authentication information cannot be obtained")
|
|
443
|
+
return dict(warn="Unable to execute command because authentication information cannot be obtained")
|
|
444
|
+
groups = scope["req"].session["signin"]["groups"]
|
|
445
|
+
ret_tools = []
|
|
446
|
+
# システムコマンドリストのフィルタリング
|
|
447
|
+
for func in func_tools:
|
|
448
|
+
mode = [t.replace('mode=', '') for t in func.tags if t.startswith('mode=')]
|
|
449
|
+
mode = mode[0] if mode else None
|
|
450
|
+
cmd = [t.replace('cmd=', '') for t in func.tags if t.startswith('cmd=')]
|
|
451
|
+
cmd = cmd[0] if cmd else None
|
|
452
|
+
if mode is None or cmd is None:
|
|
453
|
+
logger.warning(f"Tool {func.name} does not have mode or cmd tag, skipping.")
|
|
454
|
+
continue
|
|
455
|
+
if not signin.Signin._check_cmd(signin_data, groups, mode, cmd, logger):
|
|
456
|
+
logger.warning(f"User does not have permission to use tool {func.name} (mode={mode}, cmd={cmd}), skipping.")
|
|
457
|
+
continue
|
|
458
|
+
ret_tools.append(func)
|
|
459
|
+
# ユーザーコマンドリストの取得
|
|
460
|
+
args = argparse.Namespace(data=web.data, signin_file=signin_file, groups=groups, kwd=None,
|
|
461
|
+
format=False, output_json=None, output_json_append=False,)
|
|
462
|
+
st, ret, _ = cmd_list.apprun(logger, args, time.perf_counter(), [])
|
|
463
|
+
if ret is None or 'success' not in ret or not ret['success']:
|
|
464
|
+
return ret_tools
|
|
465
|
+
for opt in ret['success']:
|
|
466
|
+
func_name = f"user_{opt['title']}"
|
|
467
|
+
mode, cmd, description = opt['mode'], opt['cmd'], opt['description'] if 'description' in opt and opt['description'] else ''
|
|
468
|
+
choices = options.get_cmd_choices(mode, cmd, False)
|
|
469
|
+
description += '\n' + options.get_cmd_attr(mode, cmd, 'description_ja' if is_japan else 'description_en')
|
|
470
|
+
# 関数の定義を生成
|
|
471
|
+
func_txt = mcp._create_func_txt(func_name, mode, cmd, is_japan, options, title=opt['title'])
|
|
472
|
+
if logger.level == logging.DEBUG:
|
|
473
|
+
logger.debug(f"generating agent tool: {func_name}")
|
|
474
|
+
func_ctx = []
|
|
475
|
+
# 関数を実行してコンテキストに追加
|
|
476
|
+
exec(func_txt,
|
|
477
|
+
dict(time=time,List=List, Path=Path, argparse=argparse, common=common, options=options, logging=logging, signin=signin,),
|
|
478
|
+
dict(func_ctx=func_ctx))
|
|
479
|
+
# 関数のスキーマを生成
|
|
480
|
+
input_schema = dict(
|
|
481
|
+
type="object",
|
|
482
|
+
properties={o['opt']: mcp._to_schema(o, is_japan) for o in choices},
|
|
483
|
+
required=[],
|
|
484
|
+
)
|
|
485
|
+
output_schema = dict(type="object", properties=dict())
|
|
486
|
+
func_tool = FunctionTool(fn=func_ctx[0], name=func_name, title=func_name.title(), description=description,
|
|
487
|
+
tags=[f"mode={mode}", f"cmd={cmd}"],
|
|
488
|
+
parameters=input_schema, output_schema=output_schema,)
|
|
489
|
+
# ツールリストに追加
|
|
490
|
+
ret_tools.append(func_tool)
|
|
491
|
+
|
|
492
|
+
return ret_tools
|
|
493
|
+
return CommandListMiddleware()
|
|
494
|
+
|
|
495
|
+
def create_mw_logging(self, logger:logging.Logger, args:argparse.Namespace) -> Any:
|
|
496
|
+
"""
|
|
497
|
+
ログ出力用のミドルウェアを作成します
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
logger (logging.Logger): ロガー
|
|
501
|
+
args (argparse.Namespace): 引数
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Any: ミドルウェア
|
|
505
|
+
"""
|
|
506
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
507
|
+
class LoggingMiddleware(Middleware):
|
|
508
|
+
async def on_message(self, context: MiddlewareContext, call_next):
|
|
509
|
+
if logger.level == logging.DEBUG:
|
|
510
|
+
logger.debug(f"MCP Processing method=`{context.method}`, source=`{context.source}`, message=`{context.message}`")
|
|
511
|
+
try:
|
|
512
|
+
result = await call_next(context)
|
|
513
|
+
if logger.level == logging.DEBUG:
|
|
514
|
+
logger.debug(f"MCP Complated method=`{context.method}`")
|
|
515
|
+
return result
|
|
516
|
+
except Exception as e:
|
|
517
|
+
logger.error(f"MCP Error method=`{context.method}`, source=`{context.source}`, message=`{context.message}`: {e}", exc_info=True)
|
|
518
|
+
raise e
|
|
519
|
+
return LoggingMiddleware()
|
|
520
|
+
|
|
521
|
+
def create_mw_reqscope(self, logger:logging.Logger, args:argparse.Namespace, web) -> Any:
|
|
522
|
+
"""
|
|
523
|
+
認証用のミドルウェアを作成します
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
logger (logging.Logger): ロガー
|
|
527
|
+
args (argparse.Namespace): 引数
|
|
528
|
+
web (Any): Web関連のオブジェクト
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Any: ミドルウェア
|
|
532
|
+
"""
|
|
533
|
+
from fastapi import Response
|
|
534
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
535
|
+
class ReqScopeMiddleware(Middleware):
|
|
536
|
+
async def on_message(self, context: MiddlewareContext, call_next):
|
|
537
|
+
signin.request_scope.set(dict(req=context.fastmcp_context.request_context.request, res=Response(), websocket=None, web=web))
|
|
538
|
+
result = await call_next(context)
|
|
539
|
+
return result
|
|
540
|
+
return ReqScopeMiddleware()
|
|
541
|
+
|
|
542
|
+
def init_agent_runner(self, logger:logging.Logger, args:argparse.Namespace, web:Any) -> Tuple[Any, Any]:
|
|
239
543
|
"""
|
|
240
544
|
エージェントの初期化を行います
|
|
241
545
|
|
|
242
546
|
Args:
|
|
243
547
|
logger (logging.Logger): ロガー
|
|
244
548
|
args (argparse.Namespace): 引数
|
|
549
|
+
web (Any): Web関連のオブジェクト
|
|
245
550
|
|
|
246
551
|
Returns:
|
|
247
552
|
Tuple[Any, Any]: ランナーとFastMCP
|
|
@@ -255,120 +560,11 @@ class Mcp:
|
|
|
255
560
|
# モジュールインポート
|
|
256
561
|
from fastmcp import FastMCP
|
|
257
562
|
from google.adk.sessions import BaseSessionService
|
|
258
|
-
mcp:FastMCP = self.create_mcpserver(args)
|
|
259
563
|
session_service:BaseSessionService = self.create_session_service(args)
|
|
260
|
-
|
|
261
|
-
tools:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return f'"{d}"' if d is not None else 'None'
|
|
265
|
-
def _t2s(o:Dict[str, Any], req=True) -> str:
|
|
266
|
-
t, m, d, r = o["type"], o["multi"], o["default"], o["required"]
|
|
267
|
-
if t == Options.T_BOOL: return ("List[bool]=[]" if m else f"bool={d}") if req else ("List[bool]" if m else f"bool")
|
|
268
|
-
if t == Options.T_DATE: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
|
|
269
|
-
if t == Options.T_DATETIME: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
|
|
270
|
-
if t == Options.T_DICT: return ("List[dict]=[]" if m else f"dict={d}") if req else ("List[dict]" if m else f"dict")
|
|
271
|
-
if t == Options.T_DIR or t == Options.T_FILE:
|
|
272
|
-
if d is not None: d = str(d).replace('\\', '/')
|
|
273
|
-
return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
|
|
274
|
-
if t == Options.T_FLOAT: return ("List[float]=[]" if m else f"float={d}") if req else ("List[float]" if m else f"float")
|
|
275
|
-
if t == Options.T_INT: return ("List[int]=[]" if m else f"int={d}") if req else ("List[int]" if m else f"int")
|
|
276
|
-
if t == Options.T_STR: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
|
|
277
|
-
if t == Options.T_TEXT: return ("List[str]=[]" if m else f"str={_ds(d)}") if req else ("List[str]" if m else f"str")
|
|
278
|
-
raise ValueError(f"Unknown type: {t} for option {o['opt']}")
|
|
279
|
-
def _arg(o:Dict[str, Any], is_japan) -> str:
|
|
280
|
-
t, d = o["type"], o["default"]
|
|
281
|
-
s = f' {o["opt"]}:'
|
|
282
|
-
if t == Options.T_DIR or t == Options.T_FILE:
|
|
283
|
-
d = str(d).replace("\\", "/")
|
|
284
|
-
s += f'{_t2s(o, False)}={d}:'
|
|
285
|
-
#s += f'Optional[{_t2s(o, False)}]={d}:'
|
|
286
|
-
s += f'{o["discription_ja"] if is_japan else o["discription_en"]}'
|
|
287
|
-
return s
|
|
288
|
-
def _coercion(a:argparse.Namespace, key:str, dval) -> str:
|
|
289
|
-
dval = f'opt["{key}"] if "{key}" in opt else ' + f'"{dval}"' if isinstance(dval, str) else dval
|
|
290
|
-
aval = args.__dict__[key] if hasattr(args, key) and args.__dict__[key] else None
|
|
291
|
-
aval = f'"{aval}"' if isinstance(aval, str) else aval
|
|
292
|
-
ret = f'opt["{key}"] = {aval}' if aval is not None else f'opt["{key}"] = {dval}'
|
|
293
|
-
return ret
|
|
294
|
-
language, _ = locale.getlocale()
|
|
295
|
-
is_japan = language.find('Japan') >= 0 or language.find('ja_JP') >= 0
|
|
296
|
-
for mode in options.get_mode_keys():
|
|
297
|
-
for cmd in options.get_cmd_keys(mode):
|
|
298
|
-
if not options.get_cmd_attr(mode, cmd, 'use_agent'):
|
|
299
|
-
continue
|
|
300
|
-
discription = options.get_cmd_attr(mode, cmd, 'discription_ja' if is_japan else 'discription_en')
|
|
301
|
-
choices = options.get_cmd_choices(mode, cmd, False)
|
|
302
|
-
if len([opt for opt in choices if 'opt' in opt and opt['opt'] == 'signin_file']) <= 0:
|
|
303
|
-
choices.append(dict(opt="signin_file", type=Options.T_FILE, default=f'.{self.ver.__appid__}/user_list.yml', required=True, multi=False, hide=True, choice=None,
|
|
304
|
-
discription_ja="サインイン可能なユーザーとパスワードを記載したファイルを指定します。省略した時は認証を要求しません。",
|
|
305
|
-
discription_en="Specify a file containing users and passwords with which they can signin. If omitted, no authentication is required."),)
|
|
306
|
-
fn = f"{mode}_{cmd}"
|
|
307
|
-
func_txt = f'def {fn}(' + ", ".join([f'{o["opt"]}:{_t2s(o, False)}' for o in choices]) + '):\n'
|
|
308
|
-
func_txt += f' """\n'
|
|
309
|
-
func_txt += f' {discription}\n'
|
|
310
|
-
func_txt += f' Args:\n'
|
|
311
|
-
func_txt += "\n".join([_arg(o, is_japan) for o in choices])
|
|
312
|
-
func_txt += f'\n'
|
|
313
|
-
func_txt += f' Returns:\n'
|
|
314
|
-
func_txt += f' Dict[str, Any]:{"処理結果" if is_japan else "Processing Result"}\n'
|
|
315
|
-
func_txt += f' """\n'
|
|
316
|
-
func_txt += f' scope = signin.get_request_scope()\n'
|
|
317
|
-
func_txt += f' logger = common.default_logger()\n'
|
|
318
|
-
func_txt += f' opt = dict()\n'
|
|
319
|
-
func_txt += f' opt["mode"] = "{mode}"\n'
|
|
320
|
-
func_txt += f' opt["cmd"] = "{cmd}"\n'
|
|
321
|
-
func_txt += f' opt["data"] = opt["data"] if hasattr(opt, "data") else common.HOME_DIR / ".{self.ver.__appid__}"\n'
|
|
322
|
-
func_txt += f' opt["format"] = False\n'
|
|
323
|
-
func_txt += f' opt["output_json"] = None\n'
|
|
324
|
-
func_txt += f' opt["output_json_append"] = False\n'
|
|
325
|
-
func_txt += f' opt["debug"] = logger.level == logging.DEBUG\n'
|
|
326
|
-
func_txt += '\n'.join([f' opt["{o["opt"]}"] = {o["opt"]}' for o in choices])+'\n'
|
|
327
|
-
func_txt += f' {_coercion(args, "host", self.default_host)}\n'
|
|
328
|
-
func_txt += f' {_coercion(args, "port", self.default_port)}\n'
|
|
329
|
-
func_txt += f' {_coercion(args, "password", self.default_pass)}\n'
|
|
330
|
-
func_txt += f' {_coercion(args, "svname", self.default_svname)}\n'
|
|
331
|
-
func_txt += f' {_coercion(args, "retry_count", 3)}\n'
|
|
332
|
-
func_txt += f' {_coercion(args, "retry_interval", 3)}\n'
|
|
333
|
-
func_txt += f' {_coercion(args, "timeout", 15)}\n'
|
|
334
|
-
func_txt += f' {_coercion(args, "output_json", None)}\n'
|
|
335
|
-
func_txt += f' {_coercion(args, "output_json_append", False)}\n'
|
|
336
|
-
func_txt += f' {_coercion(args, "stdout_log", False)}\n'
|
|
337
|
-
func_txt += f' {_coercion(args, "capture_stdout", False)}\n'
|
|
338
|
-
func_txt += f' {_coercion(args, "capture_maxsize", 1024*1024)}\n'
|
|
339
|
-
func_txt += f' {_coercion(args, "tag", None)}\n'
|
|
340
|
-
func_txt += f' {_coercion(args, "clmsg_id", None)}\n'
|
|
341
|
-
func_txt += f' opt["signin_file"] = signin_file if signin_file else ".{self.ver.__appid__}/user_list.yml"\n'
|
|
342
|
-
func_txt += f' args = argparse.Namespace(**opt)\n'
|
|
343
|
-
func_txt += f' signin_data = signin.Signin.load_signin_file(args.signin_file)\n'
|
|
344
|
-
func_txt += f' req = scope["req"] if scope["req"] is not None else scope["websocket"]\n'
|
|
345
|
-
func_txt += f' sign = signin.Signin._check_signin(req, scope["res"], signin_data, logger)\n'
|
|
346
|
-
func_txt += f' if sign is not None:\n'
|
|
347
|
-
func_txt += f' logger.warning("Unable to execute command because authentication information cannot be obtained")\n'
|
|
348
|
-
func_txt += f' return dict(warn="Unable to execute command because authentication information cannot be obtained")\n'
|
|
349
|
-
func_txt += f' groups = req.session["signin"]["groups"]\n'
|
|
350
|
-
func_txt += f' logger.info("Call agent tool `{mode}_{cmd}`:user="+str(req.session["signin"]["name"])+" groups="+str(groups)+" args="+str(args))\n'
|
|
351
|
-
func_txt += f' if not signin.Signin._check_cmd(signin_data, groups, "{mode}", "{cmd}", logger):\n'
|
|
352
|
-
func_txt += f' logger.warning("You do not have permission to execute this command.")\n'
|
|
353
|
-
func_txt += f' return dict(warn="You do not have permission to execute this command.")\n'
|
|
354
|
-
func_txt += f' feat = Options.getInstance().get_cmd_attr("{mode}", "{cmd}", "feature")\n'
|
|
355
|
-
func_txt += f' try:\n'
|
|
356
|
-
func_txt += f' st, ret, _ = feat.apprun(logger, args, time.perf_counter(), [])\n'
|
|
357
|
-
func_txt += f' return ret\n'
|
|
358
|
-
func_txt += f' except Exception as e:\n'
|
|
359
|
-
func_txt += f' logger.error("Error occurs when tool is executed:", exc_info=True)\n'
|
|
360
|
-
func_txt += f' raise e\n'
|
|
361
|
-
func_txt += f'tools.append({fn})\n'
|
|
362
|
-
if logger.level == logging.DEBUG:
|
|
363
|
-
logger.debug(f"generating agent tool: {fn}")
|
|
364
|
-
|
|
365
|
-
exec(func_txt,
|
|
366
|
-
dict(time=time,List=List, argparse=argparse, common=common, Options=Options, logging=logging, signin=signin,),
|
|
367
|
-
dict(tools=tools, mcp=mcp))
|
|
368
|
-
exec(f"@mcp.tool\n{func_txt}",
|
|
369
|
-
dict(time=time,List=List, argparse=argparse, common=common, Options=Options, logging=logging, signin=signin,),
|
|
370
|
-
dict(tools=[], mcp=mcp))
|
|
371
|
-
root_agent = self.create_agent(logger, args, tools)
|
|
564
|
+
from fastmcp.tools import FunctionTool
|
|
565
|
+
tools:List[FunctionTool] = self.create_tools(logger, args)
|
|
566
|
+
mcp:FastMCP = self.create_mcpserver(args, tools, web)
|
|
567
|
+
root_agent = self.create_agent(logger, args, [t.fn for t in tools])
|
|
372
568
|
runner = self.create_runner(logger, args, session_service, root_agent)
|
|
373
569
|
if logger.level == logging.DEBUG:
|
|
374
570
|
logger.debug(f"init_agent_runner complate.")
|
cmdbox/app/options.py
CHANGED
|
@@ -180,7 +180,7 @@ class Options:
|
|
|
180
180
|
o = [f'-{val["short"]}'] if "short" in val else []
|
|
181
181
|
o += [f'--{key}']
|
|
182
182
|
language, _ = locale.getlocale()
|
|
183
|
-
opt['help'] = val['
|
|
183
|
+
opt['help'] = val['description_en'] if language.find('Japan') < 0 and language.find('ja_JP') < 0 else val['description_ja']
|
|
184
184
|
opt['default'] = val['default']
|
|
185
185
|
if val['multi'] and val['default'] is not None:
|
|
186
186
|
raise ValueError(f'list_options: The default value must be None if multi is True. key={key}, val={val}')
|
|
@@ -256,49 +256,49 @@ class Options:
|
|
|
256
256
|
self._options = dict()
|
|
257
257
|
self._options["version"] = dict(
|
|
258
258
|
short="v", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=None,
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
description_ja="バージョン表示",
|
|
260
|
+
description_en="Display version")
|
|
261
261
|
self._options["useopt"] = dict(
|
|
262
262
|
short="u", type=Options.T_STR, default=None, required=False, multi=False, hide=True, choice=None,
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
description_ja="オプションを保存しているファイルを使用します。",
|
|
264
|
+
description_en="Use the file that saves the options.")
|
|
265
265
|
self._options["saveopt"] = dict(
|
|
266
266
|
short="s", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True, choice=[True, False],
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
description_ja="指定しているオプションを `-u` で指定したファイルに保存します。",
|
|
268
|
+
description_en="Save the specified options to the file specified by `-u`.")
|
|
269
269
|
self._options["debug"] = dict(
|
|
270
270
|
short="d", type=Options.T_BOOL, default=False, required=False, multi=False, hide=True, choice=[True, False],
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
description_ja="デバックモードで起動します。",
|
|
272
|
+
description_en="Starts in debug mode.")
|
|
273
273
|
self._options["format"] = dict(
|
|
274
274
|
short="f", type=Options.T_BOOL, default=None, required=False, multi=False, hide=True,
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
description_ja="処理結果を見やすい形式で出力します。指定しない場合json形式で出力します。",
|
|
276
|
+
description_en="Output the processing result in an easy-to-read format. If not specified, output in json format.",
|
|
277
277
|
choice=None)
|
|
278
278
|
self._options["mode"] = dict(
|
|
279
279
|
short="m", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
description_ja="起動モードを指定します。",
|
|
281
|
+
description_en="Specify the startup mode.",
|
|
282
282
|
choice=[])
|
|
283
283
|
self._options["cmd"] = dict(
|
|
284
284
|
short="c", type=Options.T_STR, default=None, required=True, multi=False, hide=True,
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
description_ja="コマンドを指定します。",
|
|
286
|
+
description_en="Specify the command.",
|
|
287
287
|
choice=[])
|
|
288
288
|
self._options["tag"] = dict(
|
|
289
289
|
short="t", type=Options.T_STR, default=None, required=False, multi=True, hide=True,
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
description_ja="このコマンドのタグを指定します。",
|
|
291
|
+
description_en="Specify the tag for this command.",
|
|
292
292
|
choice=None)
|
|
293
293
|
self._options["clmsg_id"] = dict(
|
|
294
294
|
type=Options.T_STR, default=None, required=False, multi=False, hide=True,
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
description_ja="クライアントのメッセージIDを指定します。省略した場合はuuid4で生成されます。",
|
|
296
|
+
description_en="Specifies the message ID of the client. If omitted, uuid4 will be generated.",
|
|
297
297
|
choice=None)
|
|
298
298
|
self._options["description"] = dict(
|
|
299
299
|
type=Options.T_TEXT, default=None, required=False, multi=False, hide=True,
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
description_ja="このコマンド登録の説明文を指定します。Agentがこのコマンドの用途を理解するのに使用します。",
|
|
301
|
+
description_en="Specifies a description of this command registration, used to help the Agent understand the use of this command.",
|
|
302
302
|
choice=None)
|
|
303
303
|
|
|
304
304
|
def init_debugoption(self):
|
cmdbox/app/web.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from cmdbox.app import common, options
|
|
2
2
|
from cmdbox.app.commons import module
|
|
3
|
-
from fastapi import FastAPI, Request, Response
|
|
3
|
+
from fastapi import FastAPI, Request, Response, WebSocket
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from starlette.applications import Starlette
|
|
6
6
|
from starlette.middleware.sessions import SessionMiddleware
|
|
@@ -34,12 +34,12 @@ class ClientTime(feature.Feature):
|
|
|
34
34
|
"""
|
|
35
35
|
return dict(
|
|
36
36
|
type=Options.T_STR, default=None, required=False, multi=False, hide=False, use_redis=self.USE_REDIS_FALSE,
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
description_ja="クライアント側の現在時刻を表示します。",
|
|
38
|
+
description_en="Displays the current time at the client side.",
|
|
39
39
|
choice=[
|
|
40
40
|
dict(opt="timedelta", type=Options.T_INT, default=9, required=False, multi=False, hide=False, choice=None,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
description_ja="時差の時間数を指定します。",
|
|
42
|
+
description_en="Specify the number of hours of time difference."),
|
|
43
43
|
])
|
|
44
44
|
|
|
45
45
|
def apprun(self, logger:logging.Logger, args:argparse.Namespace, tm:float, pf:List[Dict[str, float]]=[]) -> Tuple[int, Dict[str, Any], Any]:
|